From d4e68de34959883d8ba10cd68e1a2a735be1d27d Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 10 Feb 2017 15:32:30 -0700 Subject: [PATCH 01/35] setting up dashboard :) --- client/coral-admin/src/AppRouter.js | 2 + .../coral-admin/src/components/ui/Header.js | 40 +++++++++++++------ .../src/containers/Dashboard/Dashboard.js | 15 +++++++ .../coral-admin/src/graphql/queries/index.js | 4 ++ .../src/graphql/queries/mostFlags.graphql | 3 ++ client/coral-admin/src/translations.json | 2 + graph/loaders/assets.js | 10 +++++ graph/resolvers/root_query.js | 10 ++++- graph/typeDefs.graphql | 40 +++++++++++++++++++ 9 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 client/coral-admin/src/containers/Dashboard/Dashboard.js create mode 100644 client/coral-admin/src/graphql/queries/index.js create mode 100644 client/coral-admin/src/graphql/queries/mostFlags.graphql diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9721123fa..e76690417 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -8,6 +8,7 @@ import CommentStream from 'containers/CommentStream/CommentStream'; import InstallContainer from 'containers/Install/InstallContainer'; import CommunityContainer from 'containers/Community/CommunityContainer'; import ModerationContainer from 'containers/ModerationQueue/ModerationContainer'; +import Dashboard from 'containers/Dashboard/Dashboard'; const routes = (
@@ -18,6 +19,7 @@ const routes = ( +
); diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 3e6598d6e..7263c7ca4 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -13,21 +13,35 @@ export default ({handleLogout, restricted = false}) => ( !restricted ?
- - {lang.t('configure.moderate')} - - - {lang.t('configure.community')} + + {lang.t('configure.moderate')} + + + {lang.t('configure.community')} - - {lang.t('configure.configure')} + + {lang.t('configure.configure')} - - {lang.t('configure.streams')} + + {lang.t('configure.streams')} + + + {lang.t('configure.dashboard')}
diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js new file mode 100644 index 000000000..c287d5f2c --- /dev/null +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -0,0 +1,15 @@ +import React from 'react'; +import {compose} from 'react-apollo'; +import {mostFlags} from 'coral-admin/src/graphql/queries'; + +class Dashboard extends React.Component { + render () { + return ( +
Dashboard
+ ); + } +} + +export default compose( + mostFlags +)(Dashboard); diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js new file mode 100644 index 000000000..30550e48a --- /dev/null +++ b/client/coral-admin/src/graphql/queries/index.js @@ -0,0 +1,4 @@ +import {graphql} from 'react-apollo'; +import MOST_FLAGS from './mostFlags.graphql'; + +export const mostFlags = graphql(MOST_FLAGS, {}); diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql new file mode 100644 index 000000000..b84e21d08 --- /dev/null +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -0,0 +1,3 @@ +query mostFlags { + metric +} diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 0eb1ab98d..68658d4a0 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -48,6 +48,7 @@ "copy": "Copy to Clipboard" }, "configure": { + "dashboard": "Dashboard", "enable-pre-moderation": "Enable pre-moderation", "enable-pre-moderation-text": "Moderators must approve any comment before it is published.", "require-email-verification": "Require Email Verification", @@ -157,6 +158,7 @@ "username_flags": "" }, "configure": { + "dashboard": "Panel", "enable-pre-moderation": "Habilitar pre-moderación", "enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.", "require-email-verification": "Necesita confirmación de correo", diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index a07a18a3f..ba8fb3751 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -46,6 +46,15 @@ const findOrCreateAssetByURL = (context, asset_url) => { }); }; +const getAssetsForMetrics = ({loaders: {Actions, Comments}}) => { + return Actions.getByTypes({action_type: 'FLAG', item_type: 'COMMENT'}) + .then((actions) => { // ALL ACTIONS :O + const ids = actions.map(({item_id}) => item_id); + + return Comments.getByQuery({ids}); + }); +}; + /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request @@ -59,6 +68,7 @@ module.exports = (context) => ({ getByURL: (url) => findOrCreateAssetByURL(context, url), getByID: new DataLoader((ids) => genAssetsByID(context, ids)), + getForMetrics: () => getAssetsForMetrics(context), getAll: new util.SingletonResolver(() => AssetModel.find({})) } }); diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 8aa14d5c6..72d57ed14 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -34,7 +34,7 @@ const RootQuery = { // Map the actions from the items referenced byt this query. The actions // returned by this query are explicitly going to be distinct by their // `item_id`'s. - let ids = actions.map((action) => action.item_id); + let ids = actions.map(({item_id}) => item_id); // Perform the query using the available resolver. return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort}); @@ -44,6 +44,14 @@ const RootQuery = { return Comments.getByQuery(query); }, + metric(_, args, {user, loaders: {Assets}}) { + if (user == null || !user.hasRoles('ADMIN')) { + return null; + } + + return Assets.getForMetrics(); + }, + // This returns the current user, ensure that if we aren't logged in, we // return null. me(_, args, {user}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index e231bc0c3..7cbb6bd47 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -167,6 +167,36 @@ interface ActionSummary { current_user: Action } +# A summary of actions for a specific action type on an Asset. +interface AssetActionSummary { + + # Number of actions associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the actions. + actionableItemCount: Int +} + +# A summary of counts related to all the Likes on an Asset. +type LikeAssetActionSummary implements AssetActionSummary { + + # Number of actions associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the actions. + actionableItemCount: Int +} + +# A summary of counts related to all the Flags on an Asset. +type FlagAssetActionSummary implements AssetActionSummary { + + # Number of actions associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the actions. + actionableItemCount: Int +} + # LikeAction is used by users who "like" a specific entity. type LikeAction implements Action { @@ -294,6 +324,13 @@ type Asset { # The date that the asset was created. created_at: Date + + # Summary of all Actions against all entities associated with the Asset. + # (likes, flags, etc.) + action_summaries: [AssetActionSummary] + + # Unique users that have commented on this Asset. + authorCount: Int } ################################################################################ @@ -358,6 +395,9 @@ type RootQuery { # The currently logged in user based on the request. me: User + + # metrics + metric: [Asset] } ################################################################################ From b16bdafc1ab6d0e34a4fe39f9228e0dc1b816c27 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 13 Feb 2017 12:22:05 -0800 Subject: [PATCH 02/35] Adds plugin for question box. --- client/coral-embed-stream/src/Embed.js | 4 ++++ client/coral-plugin-questionbox/QuestionBox.js | 10 ++++++++++ models/setting.js | 8 ++++++++ 3 files changed, 22 insertions(+) create mode 100644 client/coral-plugin-questionbox/QuestionBox.js diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 6cc783746..d33fbfce9 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -112,6 +112,10 @@ class Embed extends Component { content={asset.settings.infoBoxContent} enable={asset.settings.infoBoxEnable} /> + +
+ {content} +
; + +export default QuestionBox; diff --git a/models/setting.js b/models/setting.js index adbf60d83..4e5aa4476 100644 --- a/models/setting.js +++ b/models/setting.js @@ -28,6 +28,14 @@ const SettingSchema = new Schema({ type: String, default: '' }, + questionBoxEnable: { + type: Boolean, + default: false + }, + questionBoxContent: { + type: String, + default: '' + }, organizationName: { type: String }, From 5a2e4b708b8a047ff645dc3b5cbeacb9ff489508 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 13 Feb 2017 13:48:03 -0800 Subject: [PATCH 03/35] Adds question box --- client/coral-embed-stream/src/Embed.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index d33fbfce9..e8a7859af 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -16,6 +16,7 @@ import {Notification, notificationActions, authActions, assetActions, pym} from import Stream from './Stream'; import InfoBox from 'coral-plugin-infobox/InfoBox'; +import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import Count from 'coral-plugin-comment-count/CommentCount'; import CommentBox from 'coral-plugin-commentbox/CommentBox'; import UserBox from 'coral-sign-in/components/UserBox'; From 1b788b98c601ef9f4132bbbe304f211222b83238 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 13 Feb 2017 14:12:32 -0800 Subject: [PATCH 04/35] Adds questionBox to the gl query --- client/coral-framework/graphql/queries/streamQuery.graphql | 2 ++ client/coral-plugin-stream/Stream.js | 2 +- graph/typeDefs.graphql | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index 583a2db13..31e125d81 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -11,6 +11,8 @@ query AssetQuery($asset_url: String!) { moderation infoBoxEnable infoBoxContent + questionBoxEnable + questionBoxContent closeTimeout closedMessage charCountEnable diff --git a/client/coral-plugin-stream/Stream.js b/client/coral-plugin-stream/Stream.js index c62ef7081..2796f1247 100644 --- a/client/coral-plugin-stream/Stream.js +++ b/client/coral-plugin-stream/Stream.js @@ -51,7 +51,7 @@ class Stream extends Component { } } -// Initialize GraphQL queries or mutations with the `gql` tag +// Initialize GraphQL queries or mutations with the gql tag const StreamQuery = gql`fragment commentView on Comment { id body diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index ddffcd708..728cead8f 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -271,6 +271,8 @@ type Settings { infoBoxEnable: Boolean infoBoxContent: String + questionBoxEnable: Boolean + questionBoxContent: String closeTimeout: Int closedMessage: String charCountEnable: Boolean From 1d70ae0decb689240b870221acaa64ae064f90d8 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 13 Feb 2017 14:31:56 -0800 Subject: [PATCH 05/35] Adds question enable checkbox. --- .../components/ConfigureCommentStream.js | 12 ++++++++++++ .../containers/ConfigureStreamContainer.js | 8 +++++++- client/coral-configure/translations.json | 8 ++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index bf4ebe3e6..e986e8d0d 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -49,6 +49,18 @@ export default ({handleChange, handleApply, changed, ...props}) => ( */} +
  • + +
  • diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index dfe94de42..54dd31f4e 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -27,12 +27,14 @@ class ConfigureStreamContainer extends Component { e.preventDefault(); const {elements} = e.target; const premod = elements.premod.checked; + const questionBoxEnable = elements.qboxenable.checked; // const premodLinks = elements.premodLinks.checked; const {changed} = this.state; const newConfig = { - moderation: premod ? 'PRE' : 'POST' + moderation: premod ? 'PRE' : 'POST', + questionBoxEnable: questionBoxEnable }; if (changed) { @@ -66,6 +68,8 @@ class ConfigureStreamContainer extends Component { render () { const status = this.props.asset.closedAt === null ? 'open' : 'closed'; const premod = this.props.asset.settings.moderation === 'PRE'; + const questionBoxEnable = this.props.asset.settings.questionBoxEnable; + const questionBoxContent = this.props.asset.settings.questionBoxContent; return (
    @@ -75,6 +79,8 @@ class ConfigureStreamContainer extends Component { changed={this.state.changed} premodLinks={false} premod={premod} + questionBoxEnable={questionBoxEnable} + questionBoxContent={questionBoxContent} />

    {status === 'open' ? 'Close' : 'Open'} Comment Stream

    diff --git a/client/coral-configure/translations.json b/client/coral-configure/translations.json index 5aa7f72b6..fe1045662 100644 --- a/client/coral-configure/translations.json +++ b/client/coral-configure/translations.json @@ -7,7 +7,9 @@ "enablePremod": "Enable Premoderation", "enablePremodDescription": "Moderators must approve any comment before its published.", "enablePremodLinks": "Pre-Moderate Comments Containing Links", - "enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published." + "enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published.", + "enableQuestionBox": "Enable Question Box", + "enableQuestionBoxDescription": "Commenters will see the next question at the top of the comments stream." } }, "es": { @@ -18,7 +20,9 @@ "enablePremod": "Activar Pre Moderación", "enablePremodDescription": "Los Moderadores deben aprobar cualquier comentario antes de su publicación", "enablePremodLinks": "Pre-Moderar Commentarios que contienen Links", - "enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación." + "enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación.", + "enableQuestionBox": "Activar Caja de Pregunta", + "enableQuestionBoxDescription": "La siguiente pregunta estara en la parte de arriba del hilo de comentarios." } } } From d4109c8fcac50fd42bbe697f4a3fb9c153c7a2fe Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 13 Feb 2017 16:51:31 -0800 Subject: [PATCH 06/35] Adds textfield to coral-UI. --- .../components/ConfigureCommentStream.css | 14 ++++++++++++++ .../components/ConfigureCommentStream.js | 6 +++++- .../containers/ConfigureStreamContainer.js | 6 +++++- client/coral-configure/translations.json | 6 ++++-- client/coral-ui/components/TextField.js | 11 +++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 client/coral-ui/components/TextField.js diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-configure/components/ConfigureCommentStream.css index 8526edc09..dfa860995 100644 --- a/client/coral-configure/components/ConfigureCommentStream.css +++ b/client/coral-configure/components/ConfigureCommentStream.css @@ -35,3 +35,17 @@ p { .wrapper { margin-bottom: 20px; } + +.configSettingQuestionBox { + min-height: 100px; + margin-bottom: 20px; + cursor: pointer; + width: auto; + height: auto; + text-align: left; + overflow: visible; +} + +.hidden { + display: none; +} diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index e986e8d0d..4298384e0 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -60,9 +60,13 @@ export default ({handleChange, handleApply, changed, ...props}) => ( title: lang.t('configureCommentStream.enableQuestionBox'), description: lang.t('configureCommentStream.enableQuestionBoxDescription') }} /> +
    +
    + Pepe +
    +
    - ); diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 54dd31f4e..2f0b01800 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -28,13 +28,17 @@ class ConfigureStreamContainer extends Component { const {elements} = e.target; const premod = elements.premod.checked; const questionBoxEnable = elements.qboxenable.checked; + const questionBoxContent = ''; // elements.qboxcontent.value; + + console.log('debug ', questionBoxContent); // const premodLinks = elements.premodLinks.checked; const {changed} = this.state; const newConfig = { moderation: premod ? 'PRE' : 'POST', - questionBoxEnable: questionBoxEnable + questionBoxEnable, + questionBoxContent }; if (changed) { diff --git a/client/coral-configure/translations.json b/client/coral-configure/translations.json index fe1045662..bd45d6159 100644 --- a/client/coral-configure/translations.json +++ b/client/coral-configure/translations.json @@ -9,7 +9,8 @@ "enablePremodLinks": "Pre-Moderate Comments Containing Links", "enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published.", "enableQuestionBox": "Enable Question Box", - "enableQuestionBoxDescription": "Commenters will see the next question at the top of the comments stream." + "enableQuestionBoxDescription": "Commenters will see a question at the top of the comments stream.", + "includeQuestionHere": "Include your question here." } }, "es": { @@ -22,7 +23,8 @@ "enablePremodLinks": "Pre-Moderar Commentarios que contienen Links", "enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación.", "enableQuestionBox": "Activar Caja de Pregunta", - "enableQuestionBoxDescription": "La siguiente pregunta estara en la parte de arriba del hilo de comentarios." + "enableQuestionBoxDescription": "Una pregunta estara en la parte de arriba del hilo de comentarios.", + "includeQuestionHere": "Escribir la pregunta aquí." } } } diff --git a/client/coral-ui/components/TextField.js b/client/coral-ui/components/TextField.js new file mode 100644 index 000000000..9cdddf5c8 --- /dev/null +++ b/client/coral-ui/components/TextField.js @@ -0,0 +1,11 @@ +import React from 'react'; +import {Textfield as TextFieldMDL} from 'react-mdl'; + +const TextField = ({onChange, label, rows}) => ( + +); + +export default TextField; From 0bef413a3a7efa0c8195bbecdaab0eb7f9f62355 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 14 Feb 2017 15:20:28 -0700 Subject: [PATCH 07/35] saving my place --- client/coral-admin/src/AppRouter.js | 1 + .../src/containers/Dashboard/Dashboard.css | 23 +++++++++++++++++++ .../src/containers/Dashboard/Dashboard.js | 15 +++++++++++- .../src/graphql/queries/mostFlags.graphql | 4 +++- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 client/coral-admin/src/containers/Dashboard/Dashboard.css diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 33baa4d7b..37616a207 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -22,6 +22,7 @@ const routes = ( +
    ); diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.css b/client/coral-admin/src/containers/Dashboard/Dashboard.css new file mode 100644 index 000000000..be2cd9f7a --- /dev/null +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.css @@ -0,0 +1,23 @@ +.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 { + margin: 0; + font-size: 1.5rem; + font-weight: bold; +} diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index c287d5f2c..e01eec319 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -1,11 +1,24 @@ import React from 'react'; import {compose} from 'react-apollo'; import {mostFlags} from 'coral-admin/src/graphql/queries'; +import styles from './Dashboard.css'; class Dashboard extends React.Component { render () { return ( -
    Dashboard
    +
    +
    +

    Top Ten Articles with the most flagged comments

    + + + + +
    ArticleFlags
    +
    +
    +

    Top ten comments with the most likes

    +
    +
    ); } } diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index b84e21d08..229d64541 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -1,3 +1,5 @@ query mostFlags { - metric + metric { + id + } } From 50e3e733c3e06b8aaeb907574c06dff6d1c0bb91 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 14 Feb 2017 16:12:38 -0700 Subject: [PATCH 08/35] create FlagWidget --- .../coral-admin/src/components/FlagWidget.css | 5 ++++ .../coral-admin/src/components/FlagWidget.js | 26 +++++++++++++++++++ .../src/containers/Dashboard/Dashboard.js | 23 +++++++++++----- 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 client/coral-admin/src/components/FlagWidget.css create mode 100644 client/coral-admin/src/components/FlagWidget.js diff --git a/client/coral-admin/src/components/FlagWidget.css b/client/coral-admin/src/components/FlagWidget.css new file mode 100644 index 000000000..590aa51d1 --- /dev/null +++ b/client/coral-admin/src/components/FlagWidget.css @@ -0,0 +1,5 @@ +.heading { + margin: 0; + font-size: 1.5rem; + font-weight: bold; +} diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js new file mode 100644 index 000000000..be167de8a --- /dev/null +++ b/client/coral-admin/src/components/FlagWidget.js @@ -0,0 +1,26 @@ +import React, {PropTypes} from 'react'; +import styles from './FlagWidget.css'; + +const FlagWidget = props => { + return ( + + + + + + {/* display asset list as rows here */} + +
    ArticleFlags
    + ); +}; + +FlagWidget.propTypes = { + assets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + url: PropTypes.string, + count: PropTypes.number + })).isRequired +}; + +export default FlagWidget; diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index e01eec319..4d7f268f8 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -1,21 +1,30 @@ 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'; class Dashboard extends React.Component { render () { + + const {data} = this.props; + + if (data.loading) { + return ; + } + + if (data.error) { + return
    {data.error}
    ; + } + return (
    -
    +

    Top Ten Articles with the most flagged comments

    - - - - -
    ArticleFlags
    +
    -
    +

    Top ten comments with the most likes

    From 00c2305ff73d38c7dc28c9fbc02754cb2937458d Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 14 Feb 2017 18:29:59 -0500 Subject: [PATCH 09/35] Adding view more button to replies. --- client/coral-embed-stream/src/Comment.js | 13 +++++++++ client/coral-embed-stream/src/Embed.js | 1 + client/coral-embed-stream/src/LoadMore.js | 15 ++++++---- client/coral-embed-stream/src/Stream.js | 2 ++ client/coral-embed-stream/style/default.css | 10 +++++++ .../coral-framework/graphql/queries/index.js | 28 +++++++++++++++---- .../graphql/queries/loadMore.graphql | 1 + .../graphql/queries/streamQuery.graphql | 1 + 8 files changed, 60 insertions(+), 11 deletions(-) diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 3bf787a9c..034c44f6a 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -17,6 +17,7 @@ import PubDate from 'coral-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; import FlagComment from 'coral-plugin-flags/FlagComment'; import LikeButton from 'coral-plugin-likes/LikeButton'; +import LoadMore from 'coral-embed-stream/src/LoadMore'; import styles from './Comment.css'; @@ -89,6 +90,7 @@ class Comment extends React.Component { showSignInDialog, postLike, postFlag, + loadMore, setActiveReplyBox, activeReplyBox, deleteAction @@ -172,6 +174,17 @@ class Comment extends React.Component { comment={reply} />; }) } + { + comment.replies && +
    + comment.replies.length} + loadMore={loadMore}/> +
    + }
    ); } diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index e5eec37cb..d58107934 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -151,6 +151,7 @@ class Embed extends Component { currentUser={user} postLike={this.props.postLike} postFlag={this.props.postFlag} + loadMore={this.props.loadMore} deleteAction={this.props.deleteAction} showSignInDialog={this.props.showSignInDialog} comments={asset.comments} /> diff --git a/client/coral-embed-stream/src/LoadMore.js b/client/coral-embed-stream/src/LoadMore.js index e871af410..e07c1aa35 100644 --- a/client/coral-embed-stream/src/LoadMore.js +++ b/client/coral-embed-stream/src/LoadMore.js @@ -4,24 +4,29 @@ import translations from 'coral-framework/translations.json'; import {Button} from 'coral-ui'; const lang = new I18n(translations); -const loadMoreComments = (id, comments, loadMore) => { +const loadMoreComments = (id, comments, loadMore, parentId) => { if (!comments.length) { return; } + const cursor = parentId + ? comments[1].created_at + : comments[comments.length - 1].created_at; + loadMore({ limit: 10, - cursor: comments[comments.length - 1].created_at, + cursor, asset_id: id, - sort: 'REVERSE_CHRONOLOGICAL' + parent_id: parentId, + sort: parentId ? 'CHRONOLOGICAL' : 'REVERSE_CHRONOLOGICAL' }); }; -const LoadMore = ({id, comments, loadMore, moreComments}) => moreComments ? +const LoadMore = ({id, comments, loadMore, moreComments, parentId}) => moreComments ?
    diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 2f0b01800..9092a2673 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -28,9 +28,7 @@ class ConfigureStreamContainer extends Component { const {elements} = e.target; const premod = elements.premod.checked; const questionBoxEnable = elements.qboxenable.checked; - const questionBoxContent = ''; // elements.qboxcontent.value; - - console.log('debug ', questionBoxContent); + const questionBoxContent = elements.qboxcontent.value; // const premodLinks = elements.premodLinks.checked; const {changed} = this.state; @@ -51,7 +49,10 @@ class ConfigureStreamContainer extends Component { } } - handleChange () { + handleChange (e) { + if (e.target && e.target.id === 'qboxenable') { + this.props.asset.settings.questionBoxEnable = e.target.checked; + } this.setState({ changed: true }); @@ -104,7 +105,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = dispatch => ({ updateStatus: status => dispatch(updateOpenStatus(status)), - updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig)) + updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig)), }); export default compose( diff --git a/client/coral-configure/translations.json b/client/coral-configure/translations.json index bd45d6159..a40c12bc1 100644 --- a/client/coral-configure/translations.json +++ b/client/coral-configure/translations.json @@ -8,22 +8,22 @@ "enablePremodDescription": "Moderators must approve any comment before its published.", "enablePremodLinks": "Pre-Moderate Comments Containing Links", "enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published.", - "enableQuestionBox": "Enable Question Box", - "enableQuestionBoxDescription": "Commenters will see a question at the top of the comments stream.", - "includeQuestionHere": "Include your question here." + "enableQuestionBox": "Ask readers a question", + "enableQuestionBoxDescription": "This question will appear at the top of this comment stram. Ask readers about a certain issue in the article or pose discussion questions, etc.", + "includeQuestionHere": "Write your question here." } }, "es": { "configureCommentStream": { "apply": "Aplicar", "title": "Configurar los comentarios", - "description": "Como Administrador puedes modificar las opciones de los comentarios en este artículo", + "description": "Como Administrador/a puedes modificar las opciones de los comentarios en este artículo", "enablePremod": "Activar Pre Moderación", - "enablePremodDescription": "Los Moderadores deben aprobar cualquier comentario antes de su publicación", + "enablePremodDescription": "Los y las Moderadoras deben aprobar cualquier comentario antes de su publicación", "enablePremodLinks": "Pre-Moderar Commentarios que contienen Links", - "enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación.", - "enableQuestionBox": "Activar Caja de Pregunta", - "enableQuestionBoxDescription": "Una pregunta estara en la parte de arriba del hilo de comentarios.", + "enablePremodLinksDescription": "Los y las Moderadoras deben probar cualquier comentario que contengan links antes de su publicación.", + "enableQuestionBox": "Hacer una pregunta a los y las lectoras.", + "enableQuestionBoxDescription": "Esta pregunta aparecera en la parte de arriba del hilo de comentarios.", "includeQuestionHere": "Escribir la pregunta aquí." } } diff --git a/client/coral-ui/components/TextField.js b/client/coral-ui/components/TextField.js index 9cdddf5c8..80855dbf1 100644 --- a/client/coral-ui/components/TextField.js +++ b/client/coral-ui/components/TextField.js @@ -1,11 +1,21 @@ -import React from 'react'; +import React, {Component} from 'react'; import {Textfield as TextFieldMDL} from 'react-mdl'; +import 'material-design-lite'; -const TextField = ({onChange, label, rows}) => ( - -); - -export default TextField; +export default class TextField extends Component { + render() { + const {className, onChange, rows, questionBoxContent} = this.props; + return ( + + ); + } +} diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index beb2396f5..bfc5acb16 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -14,6 +14,7 @@ export {default as List} from './components/List'; export {default as Item} from './components/Item'; export {default as Card} from './components/Card'; export {default as FormField} from './components/FormField'; +export {default as TextField} from './components/TextField'; export {default as Success} from './components/Success'; export {default as Pager} from './components/Pager'; export {default as Wizard} from './components/Wizard'; From fbf543727b374882cdd864e2791cce7adcede838 Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 14 Feb 2017 18:54:51 -0500 Subject: [PATCH 11/35] Moving load more limit to constant. --- client/coral-embed-stream/src/LoadMore.js | 3 ++- client/coral-framework/constants/comments.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 client/coral-framework/constants/comments.js diff --git a/client/coral-embed-stream/src/LoadMore.js b/client/coral-embed-stream/src/LoadMore.js index e07c1aa35..ff56f6065 100644 --- a/client/coral-embed-stream/src/LoadMore.js +++ b/client/coral-embed-stream/src/LoadMore.js @@ -1,6 +1,7 @@ import React from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations.json'; +import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments'; import {Button} from 'coral-ui'; const lang = new I18n(translations); @@ -15,7 +16,7 @@ const loadMoreComments = (id, comments, loadMore, parentId) => { : comments[comments.length - 1].created_at; loadMore({ - limit: 10, + limit: ADDTL_COMMENTS_ON_LOAD_MORE, cursor, asset_id: id, parent_id: parentId, diff --git a/client/coral-framework/constants/comments.js b/client/coral-framework/constants/comments.js new file mode 100644 index 000000000..17ea2223e --- /dev/null +++ b/client/coral-framework/constants/comments.js @@ -0,0 +1 @@ +export const ADDTL_COMMENTS_ON_LOAD_MORE = 10; From e640bc2cc0581321ea7f68c5f0ea1f3aaf124c7d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 14 Feb 2017 17:02:50 -0700 Subject: [PATCH 12/35] Added edge for metrics, fixed client linting errors --- .../coral-admin/src/components/FlagWidget.js | 3 +- graph/loaders/index.js | 2 + graph/loaders/metrics.js | 137 ++++++++++++++++++ graph/resolvers/asset_action_summary.js | 10 ++ graph/resolvers/index.js | 2 + graph/resolvers/root_query.js | 4 +- graph/typeDefs.graphql | 23 +-- 7 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 graph/loaders/metrics.js create mode 100644 graph/resolvers/asset_action_summary.js diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index be167de8a..324b76c10 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -1,7 +1,6 @@ import React, {PropTypes} from 'react'; -import styles from './FlagWidget.css'; -const FlagWidget = props => { +const FlagWidget = () => { return ( diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 536e40fa9..5b1894b65 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const Actions = require('./actions'); const Assets = require('./assets'); const Comments = require('./comments'); +const Metrics = require('./metrics'); const Settings = require('./settings'); const Users = require('./users'); @@ -18,6 +19,7 @@ module.exports = (context) => { Actions, Assets, Comments, + Metrics, Settings, Users ].map((loaders) => { diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js new file mode 100644 index 000000000..a827ddeea --- /dev/null +++ b/graph/loaders/metrics.js @@ -0,0 +1,137 @@ +const _ = require('lodash'); +const CommentModel = require('../../models/comment'); +const AssetModel = require('../../models/asset'); +const ActionModel = require('../../models/action'); + +const getMetrics = (context, {from, to}) => { + + let commentMetrics = {}; + let assetMetrics = []; + + return ActionModel.aggregate([ + + // Find all actions that were created in the time range. + {$match: { + action_type: 'FLAG', + item_type: 'COMMENTS', + created_at: { + $gt: from, + $lt: to + } + }}, + + // Count all those items. + {$group: { + _id: '$item_id', + count: { + $sum: 1 + } + }}, + + // Project the count to a better field. + {$project: { + item_id: '$_id', + count: '$count' + }} + ]).then((actionSummaries) => { + + // Collect all the action summaries into a dictionary. + actionSummaries.forEach((actionSummary) => { + commentMetrics[actionSummary.item_id] = actionSummary.count; + }); + + // Collect just the comment id's. + let commentIDs = actionSummaries.map((as) => as.item_id); + + // Find those comments. + return CommentModel.aggregate([ + + // Get only those comments. + {$match: { + id: { + $in: commentIDs + } + }}, + + // Group by their asset id and push in the comment id. + {$group: { + _id: { + asset_id: '$asset_id' + }, + ids: { + $addToSet: '$id' + } + }}, + + // Project that data only as better fields. + {$project: { + asset_id: '$_id.asset_id', + ids: '$ids' + }} + ]); + }) + .then((commentResults) => { + + // Compute all the action summaries for the assets based on the time slice + // that you requested. + commentResults.forEach((result) => { + let actionCount = 0; + + result.ids.forEach((id) => { + actionCount += commentMetrics[id]; + }); + + assetMetrics.push({ + id: result.asset_id, + actionCount, + actionableItemCount: result.ids.length + }); + }); + + // Sort the assets by flag count. + assetMetrics.sort((a, b) => { + return b.flags - a.flags; + }); + + // Only keep the top 10. + assetMetrics = assetMetrics.slice(0, 10); + + // Determine the assets that we need to return. + return AssetModel.find({ + id: { + $in: assetMetrics.map((asset) => asset.id) + } + }); + }) + .then((assets) => { + + // 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, actionCount, actionableItemCount}) => { + if (id in groupedAssets) { + let asset = groupedAssets[id][0]; + + let flagAssetActionSummary = { + action_type: 'FLAG', + actionCount, + actionableItemCount + }; + + // Add the action summaries to the asset. + asset.action_summaries = [flagAssetActionSummary]; + + return asset; + } + + return null; + }).filter((asset) => asset != null); + }); +}; + +module.exports = (context) => ({ + Metrics: { + get: ({from, to}) => getMetrics(context, {from, to}) + } +}); diff --git a/graph/resolvers/asset_action_summary.js b/graph/resolvers/asset_action_summary.js new file mode 100644 index 000000000..99483f9bb --- /dev/null +++ b/graph/resolvers/asset_action_summary.js @@ -0,0 +1,10 @@ +const AssetActionSummary = { + __resolveType({action_type}) { + switch (action_type) { + case 'FLAG': + return 'FlagAssetActionSummary'; + } + } +}; + +module.exports = AssetActionSummary; diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index 84dd10fdc..65461dc76 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -1,5 +1,6 @@ const ActionSummary = require('./action_summary'); const Action = require('./action'); +const AssetActionSummary = require('./asset_action_summary'); const Asset = require('./asset'); const Comment = require('./comment'); const Date = require('./date'); @@ -17,6 +18,7 @@ const ValidationUserError = require('./validation_user_error'); module.exports = { ActionSummary, Action, + AssetActionSummary, Asset, Comment, Date, diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index ff81e4c0f..432ad126f 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -39,12 +39,12 @@ const RootQuery = { return Comments.getByQuery(query); }, - metric(_, args, {user, loaders: {Assets}}) { + metrics(_, {from, to}, {user, loaders: {Metrics}}) { if (user == null || !user.hasRoles('ADMIN')) { return null; } - return Assets.getForMetrics(); + return Metrics.get({from, to}); }, // 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 0ba307046..5d9262612 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -198,16 +198,6 @@ interface AssetActionSummary { actionableItemCount: Int } -# A summary of counts related to all the Likes on an Asset. -type LikeAssetActionSummary implements AssetActionSummary { - - # Number of actions associated with actionable types on this this Asset. - actionCount: Int - - # Number of unique actionable types that are referenced by the actions. - actionableItemCount: Int -} - # A summary of counts related to all the Flags on an Asset. type FlagAssetActionSummary implements AssetActionSummary { @@ -343,15 +333,12 @@ type Asset { # The date that the asset was closed at. closedAt: Date - # The date that the asset was created. - created_at: Date - # Summary of all Actions against all entities associated with the Asset. # (likes, flags, etc.) action_summaries: [AssetActionSummary] - # Unique users that have commented on this Asset. - authorCount: Int + # The date that the asset was created. + created_at: Date } ################################################################################ @@ -386,7 +373,7 @@ type ValidationUserError implements UserError { } ################################################################################ -## Queries +## Queries; ################################################################################ # Establishes the ordering of the content by their created_at time stamp. @@ -424,8 +411,8 @@ type RootQuery { # The currently logged in user based on the request. me: User - # metrics - metric: [Asset] + # Metrics related to user actions are saturated into the assets returned. + metrics(from: Date!, to: Date!): [Asset] } ################################################################################ From d3f81353a6309acd13eb54441f9c6399fca6caae Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 14 Feb 2017 16:51:57 -0800 Subject: [PATCH 13/35] Fix TextField for coral-ui and setting configure stream, question box. --- .../components/ConfigureCommentStream.js | 8 +++++--- .../containers/ConfigureStreamContainer.js | 7 +++++++ client/coral-ui/components/TextField.js | 10 ++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index 3afd1336c..e621f85f1 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -7,7 +7,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations.json'; const lang = new I18n(translations); -export default ({handleChange, handleApply, changed, ...props}) => ( +export default ({handleChange, handleApply, changed, updateQuestionBoxContent, ...props}) => (
    @@ -48,10 +48,12 @@ export default ({handleChange, handleApply, changed, ...props}) => (
    diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 9092a2673..219e4d99e 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -21,6 +21,7 @@ class ConfigureStreamContainer extends Component { this.toggleStatus = this.toggleStatus.bind(this); this.handleChange = this.handleChange.bind(this); this.handleApply = this.handleApply.bind(this); + this.updateQuestionBoxContent = this.updateQuestionBoxContent.bind(this); } handleApply (e) { @@ -58,6 +59,11 @@ class ConfigureStreamContainer extends Component { }); } + updateQuestionBoxContent(e) { + this.props.asset.settings.questionBoxContent = e.target.value; + this.handleChange(e); + } + toggleStatus () { this.props.updateStatus( this.props.asset.closedAt === null ? 'closed' : 'open' @@ -84,6 +90,7 @@ class ConfigureStreamContainer extends Component { changed={this.state.changed} premodLinks={false} premod={premod} + updateQuestionBoxContent={this.updateQuestionBoxContent} questionBoxEnable={questionBoxEnable} questionBoxContent={questionBoxContent} /> diff --git a/client/coral-ui/components/TextField.js b/client/coral-ui/components/TextField.js index 80855dbf1..6c7b30ad1 100644 --- a/client/coral-ui/components/TextField.js +++ b/client/coral-ui/components/TextField.js @@ -4,17 +4,15 @@ import 'material-design-lite'; export default class TextField extends Component { render() { - const {className, onChange, rows, questionBoxContent} = this.props; + const {id, className, onChange, label, rows, value} = this.props; return ( ); } From 0f074dc034e78b900dfbccc521ea25851b4bb682 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 14 Feb 2017 17:13:08 -0800 Subject: [PATCH 14/35] Changes styles for Emma and Sam's design. --- client/coral-embed-stream/style/default.css | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 2d7f478c1..a33dcf940 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -62,7 +62,7 @@ hr { .coral-plugin-infobox-info { top: 0; border: 0; - background: rgb(105,105,105); + background: rgb(35,118,216); color: white; width: 100%; text-align: center; @@ -72,6 +72,21 @@ hr { display: block; } +/* Question Box Styles */ +.coral-plugin-questionbox-info { + top: 0; + border: 0; + background: rgb(105,105,105); + color: white; + width: 100%; + text-align: center; + padding: 10px; + margin-bottom: 0px; + font-weight: bold; + display: block; +} + + .hidden { visibility: hidden; display: none; From d7469d648de86e6c811d82a14ece42d92e2e4472 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 14 Feb 2017 17:48:10 -0800 Subject: [PATCH 15/35] Replace element with name instead of id. Add style to textfield from coral-ui. --- client/coral-configure/components/ConfigureCommentStream.js | 2 +- client/coral-ui/components/TextField.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index e621f85f1..67f959390 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -48,7 +48,7 @@ export default ({handleChange, handleApply, changed, updateQuestionBoxContent, .
    ); } From 417edc81ca09db45788240c5f982d0c5b3472cc4 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 15 Feb 2017 13:40:01 -0300 Subject: [PATCH 16/35] Admin actions for flagged comments --- .../src/containers/ModerationQueue/ModerationQueue.js | 3 ++- .../src/containers/ModerationQueue/components/styles.css | 1 + .../ModerationQueue/helpers/moderationQueueActionsMap.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 052138590..f3876d02a 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -9,12 +9,13 @@ const ModerationQueue = props => {
      { props.data[props.activeTab].map((comment, i) => { + const status = comment.action_summaries ? 'FLAGGED' : comment.status; return Date: Wed, 15 Feb 2017 13:21:34 -0500 Subject: [PATCH 17/35] Removing log statement. --- client/coral-framework/graphql/queries/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index 83d5bfb76..09b3159d0 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -26,7 +26,6 @@ export const queryStream = graphql(STREAM_QUERY, { props: ({data}) => ({ data, loadMore: ({limit, cursor, parent_id, asset_id, sort}) => { - console.log('parent_id', parent_id); return data.fetchMore({ query: LOAD_MORE, variables: { @@ -37,7 +36,7 @@ export const queryStream = graphql(STREAM_QUERY, { sort }, updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => - + // If loading more replies parent_id ? { ...oldData, From ed105f876eb2e85544f207e93324403ebc150adc Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 15 Feb 2017 16:19:31 -0300 Subject: [PATCH 18/35] not Accepted ones in the Flagged queue --- client/coral-admin/src/graphql/queries/modQueueQuery.graphql | 1 + 1 file changed, 1 insertion(+) diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql index ca7f7aa07..8f5bdd31f 100644 --- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -9,6 +9,7 @@ query ModQueue ($asset_id: ID!) { } flagged: comments(query: { action_type: FLAG, + statuses: [null, PREMOD], asset_id: $asset_id }) { ...commentView From 1d1b1b3391faa159c0885f56599377e73cb1901d Mon Sep 17 00:00:00 2001 From: gaba Date: Wed, 15 Feb 2017 11:37:39 -0800 Subject: [PATCH 19/35] Move to use FormField the right way. --- .../components/ConfigureCommentStream.css | 28 ------------------- .../components/ConfigureCommentStream.js | 9 +++--- client/coral-ui/components/TextField.js | 20 ------------- client/coral-ui/index.js | 1 - 4 files changed, 4 insertions(+), 54 deletions(-) delete mode 100644 client/coral-ui/components/TextField.js diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-configure/components/ConfigureCommentStream.css index cbe2ba715..ea20e4c05 100644 --- a/client/coral-configure/components/ConfigureCommentStream.css +++ b/client/coral-configure/components/ConfigureCommentStream.css @@ -36,34 +36,6 @@ p { margin-bottom: 20px; } -.configSettingQuestionBox { - min-height: 100px; - margin-bottom: 20px; - cursor: pointer; - width: auto; - height: auto; - text-align: left; - overflow: visible; -} - .hidden { display: none; } - -/* Question Box Styles */ - -.configSettingQuestionBox { - background: white; -} - -.configSettingQuestionBoxInfo { - top: 0; - border: 10px; - background: white; - color: inherit; - width: 100%; - text-align: left; - padding: 5px; - font-weight: bold; - display: block; -} diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index 67f959390..cc5be6832 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Button, Checkbox, TextField} from 'coral-ui'; +import {Button, Checkbox, FormField} from 'coral-ui'; import styles from './ConfigureCommentStream.css'; @@ -46,10 +46,9 @@ export default ({handleChange, handleApply, changed, updateQuestionBoxContent, . description: lang.t('configureCommentStream.enableQuestionBoxDescription') }} /> -
      - + - ); - } -} diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index bfc5acb16..beb2396f5 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -14,7 +14,6 @@ export {default as List} from './components/List'; export {default as Item} from './components/Item'; export {default as Card} from './components/Card'; export {default as FormField} from './components/FormField'; -export {default as TextField} from './components/TextField'; export {default as Success} from './components/Success'; export {default as Pager} from './components/Pager'; export {default as Wizard} from './components/Wizard'; From 68c764fd43a3e4d518608ca45effb9b1f21aa503 Mon Sep 17 00:00:00 2001 From: gaba Date: Wed, 15 Feb 2017 11:50:21 -0800 Subject: [PATCH 20/35] Rename coral-ui/FormField with TextField. --- .../src/components/BanUserDialog.css | 6 +++--- .../components/Steps/AddOrganizationName.js | 6 +++--- .../components/Steps/CreateYourAccount.js | 18 +++++++++--------- .../components/Steps/InviteTeamMembers.js | 12 ++++++------ .../Install/components/Steps/style.css | 2 +- .../components/ConfigureCommentStream.js | 4 ++-- .../components/CreateDisplayNameDialog.js | 4 ++-- .../coral-sign-in/components/ForgotContent.js | 2 +- .../coral-sign-in/components/SignInContent.js | 8 ++++---- .../coral-sign-in/components/SignUpContent.js | 10 +++++----- .../{FormField.css => TextField.css} | 6 +++--- .../components/{FormField.js => TextField.js} | 10 +++++----- client/coral-ui/index.js | 2 +- 13 files changed, 45 insertions(+), 45 deletions(-) rename client/coral-ui/components/{FormField.css => TextField.css} (93%) rename client/coral-ui/components/{FormField.js => TextField.js} (70%) diff --git a/client/coral-admin/src/components/BanUserDialog.css b/client/coral-admin/src/components/BanUserDialog.css index 054c343dd..a46b9da32 100644 --- a/client/coral-admin/src/components/BanUserDialog.css +++ b/client/coral-admin/src/components/BanUserDialog.css @@ -22,17 +22,17 @@ } } -.formField { +.textField { margin-top: 15px; } -.formField label { +.textField label { font-size: 1.08em; font-weight: bold; margin-bottom: 5px; } -.formField input { +.textField input { width: 100%; display: block; border: none; diff --git a/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js b/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js index 0f85bbad4..ae23d0048 100644 --- a/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js +++ b/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js @@ -1,6 +1,6 @@ import React from 'react'; import styles from './style.css'; -import {FormField, Button} from 'coral-ui'; +import {TextField, Button} from 'coral-ui'; const AddOrganizationName = props => { const {handleSettingsChange, handleSettingsSubmit, install} = props; @@ -12,8 +12,8 @@ const AddOrganizationName = props => {

      - { const {handleUserChange, handleUserSubmit, install} = props; @@ -8,8 +8,8 @@ const InitialStep = props => {
      - { noValidate /> - { errorMsg={install.errors.username} /> - { errorMsg={install.errors.password} /> - { const {nextStep} = props; @@ -14,19 +14,19 @@ const InviteTeamMembers = props => {
      - - -
      +
      this.emailInput = input} diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index eebaf960b..1f339979b 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,6 +1,6 @@ import React, {PropTypes} from 'react'; import Alert from './Alert'; -import {Button, FormField, Spinner, Success} from 'coral-ui'; +import {Button, TextField, Spinner, Success} from 'coral-ui'; import styles from './styles.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; @@ -32,7 +32,7 @@ const SignInContent = ({ auth.emailVerificationFailure ?

      {lang.t('signIn.requestNewVerifyEmail')}

      -
      - -
      - - - { errors.password && Password must be at least 8 characters. } - ( -
      +const TextField = ({className, showErrors = false, errorMsg, label, ...props}) => ( +
      @@ -15,7 +15,7 @@ const FormField = ({className, showErrors = false, errorMsg, label, ...props}) =
      ); -FormField.propTypes = { +TextField.propTypes = { label: PropTypes.string, value: PropTypes.string, onChange: PropTypes.func, @@ -23,4 +23,4 @@ FormField.propTypes = { type: PropTypes.string }; -export default FormField; +export default TextField; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index beb2396f5..255259c32 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -13,7 +13,7 @@ export {default as Icon} from './components/Icon'; export {default as List} from './components/List'; export {default as Item} from './components/Item'; export {default as Card} from './components/Card'; -export {default as FormField} from './components/FormField'; +export {default as TextField} from './components/TextField'; export {default as Success} from './components/Success'; export {default as Pager} from './components/Pager'; export {default as Wizard} from './components/Wizard'; From 6eda4956639254c9ba93ed317b27dbde0e463875 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 12:57:51 -0700 Subject: [PATCH 21/35] display most flagged assets in a table --- .../coral-admin/src/components/FlagWidget.css | 24 ++++++++++ .../coral-admin/src/components/FlagWidget.js | 46 ++++++++++++++----- .../src/containers/Dashboard/Dashboard.js | 3 +- .../coral-admin/src/graphql/queries/index.js | 15 +++++- .../src/graphql/queries/mostFlags.graphql | 11 ++++- client/coral-docs/src/services/fetcher.js | 2 +- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.css b/client/coral-admin/src/components/FlagWidget.css index 590aa51d1..03be150d4 100644 --- a/client/coral-admin/src/components/FlagWidget.css +++ b/client/coral-admin/src/components/FlagWidget.css @@ -3,3 +3,27 @@ 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; +} diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index 324b76c10..f01008e28 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -1,25 +1,49 @@ import React, {PropTypes} from 'react'; +import styles from './FlagWidget.css'; + +const FlagWidget = ({assets}) => { -const FlagWidget = () => { return ( -
    - - +
    ArticleFlags
    + + + + + + - {/* display asset list as rows here */} + {assets.map(asset => { + const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; + return ( + + + + + + ); + })}
    ArticleFlagsComment Count
    {asset.title}{flagCount}{asset.commentCount}
    ); }; FlagWidget.propTypes = { - assets: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - title: PropTypes.string, - url: PropTypes.string, - count: PropTypes.number - })).isRequired + 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.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index 4d7f268f8..6baa16af1 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -9,6 +9,7 @@ class Dashboard extends React.Component { render () { const {data} = this.props; + const {metrics: assets} = data; if (data.loading) { return ; @@ -22,7 +23,7 @@ class Dashboard extends React.Component {

    Top Ten Articles with the most flagged comments

    - +

    Top ten comments with the most likes

    diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index a0cceb0c7..9b8a51216 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -3,7 +3,20 @@ import {graphql} from 'react-apollo'; import MOST_FLAGS from './mostFlags.graphql'; import MOD_QUEUE_QUERY from './modQueueQuery.graphql'; -export const mostFlags = graphql(MOST_FLAGS, {}); +export const mostFlags = graphql(MOST_FLAGS, { + options: () => { + + // currently hard-coded per Greg's advice + const fiveMinutesAgo = new Date(); + fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5); + return { + variables: { + from: fiveMinutesAgo.toISOString(), + to: new Date().toISOString() + } + }; + } +}); export const modQueueQuery = graphql(MOD_QUEUE_QUERY, { options: ({params: {id = ''}}) => { diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index 229d64541..1bcec7a1e 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -1,5 +1,12 @@ -query mostFlags { - metric { +query Metrics ($from: Date!, $to: Date!) { + metrics(from: $from, to: $to) { id + title + url + commentCount + action_summaries { + actionCount + actionableItemCount + } } } diff --git a/client/coral-docs/src/services/fetcher.js b/client/coral-docs/src/services/fetcher.js index e4d6f091d..aec448762 100644 --- a/client/coral-docs/src/services/fetcher.js +++ b/client/coral-docs/src/services/fetcher.js @@ -1,5 +1,5 @@ export default function fetcher(query) { - return fetch(`${window.location.host}/api/v1/graph/ql`, { + return fetch(`${window.location.origin}/api/v1/graph/ql`, { method: 'POST', headers: { Accept: 'application/json', From 57d4f9d7570fd313a56f1be8fc40cf78224c83b7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 13:21:15 -0700 Subject: [PATCH 22/35] Changed new comment's status from `null` to `NONE`. --- graph/loaders/comments.js | 6 +++--- graph/mutators/comment.js | 6 +++--- graph/typeDefs.graphql | 8 ++++++-- models/comment.js | 8 ++++++-- routes/api/comments/index.js | 2 +- services/comments.js | 7 ++++--- test/services/comments.js | 4 ++-- 7 files changed, 25 insertions(+), 16 deletions(-) diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 23f7eb29d..24b66f4bd 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -18,7 +18,7 @@ const getCountsByAssetID = (context, asset_ids) => { $in: asset_ids }, status: { - $in: [null, 'ACCEPTED'] + $in: ['NONE', 'ACCEPTED'] }, parent_id: null } @@ -51,7 +51,7 @@ const getCountsByParentID = (context, parent_ids) => { $in: parent_ids }, status: { - $in: [null, 'ACCEPTED'] + $in: ['NONE', 'ACCEPTED'] } } }, @@ -88,7 +88,7 @@ const getCommentsByQuery = ({user}, {ids, statuses, asset_id, parent_id, author_ } else { comments = comments.where({ status: { - $in: [null, 'ACCEPTED'] + $in: ['NONE', 'ACCEPTED'] } }); } diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 98891953c..91b2b0d01 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -11,10 +11,10 @@ const Wordlist = require('../../services/wordlist'); * @param {String} body body of the comment * @param {String} asset_id asset for the comment * @param {String} parent_id optional parent of the comment - * @param {String} [status=null] the status of the new comment + * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = null) => { +const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = 'NONE') => { return CommentsService.publicCreate({ body, asset_id, @@ -105,7 +105,7 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => { if (charCountEnable && body.length > charCount) { return 'REJECTED'; } - return moderation === 'PRE' ? 'PREMOD' : null; + return moderation === 'PRE' ? 'PREMOD' : 'NONE'; }); } diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index b1c4824e8..d338c7c71 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -67,6 +67,10 @@ type Tag { # The statuses that a comment may have. enum COMMENT_STATUS { + # The comment is not PREMOD, but was not applied a moderation status by a + # moderator. + NONE + # The comment has been accepted by a moderator. ACCEPTED @@ -93,7 +97,7 @@ enum ACTION_TYPE { input CommentsQuery { # current status of a comment. - statuses: [COMMENT_STATUS] + statuses: [COMMENT_STATUS!] # asset that a comment is on. asset_id: ID @@ -152,7 +156,7 @@ type Comment { asset: Asset # The current status of a comment. - status: COMMENT_STATUS + status: COMMENT_STATUS! # The time when the comment was created created_at: Date! diff --git a/models/comment.js b/models/comment.js index ac44d904e..c88041c8a 100644 --- a/models/comment.js +++ b/models/comment.js @@ -6,7 +6,7 @@ const STATUSES = [ 'ACCEPTED', 'REJECTED', 'PREMOD', - null + 'NONE' ]; /** @@ -66,7 +66,11 @@ const CommentSchema = new Schema({ asset_id: String, author_id: String, status_history: [StatusSchema], - status: {type: String, default: null}, + status: { + type: String, + enum: STATUSES, + default: 'NONE' + }, tags: [TagSchema], parent_id: String }, { diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index 96f3911d4..e77ca0c30 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -52,7 +52,7 @@ router.get('/', (req, res, next) => { if (user_id) { query = CommentsService.findByUserId(user_id, authorization.has(req.user, 'ADMIN')); } else if (status) { - query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? null : status)); + query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? 'NONE' : status)); } else if (action_type) { query = CommentsService .findIdsByActionType(action_type) diff --git a/services/comments.js b/services/comments.js index bcc534c51..8ec2b6038 100644 --- a/services/comments.js +++ b/services/comments.js @@ -11,6 +11,7 @@ const STATUSES = [ 'ACCEPTED', 'REJECTED', 'PREMOD', + 'NONE', ]; module.exports = class CommentsService { @@ -31,7 +32,7 @@ module.exports = class CommentsService { body, asset_id, parent_id, - status = null, + status = 'NONE', author_id } = comment; @@ -146,7 +147,7 @@ module.exports = class CommentsService { * @param {String} status status of the comment to search for * @return {Promise} resovles to comment array */ - static findByStatus(status = null) { + static findByStatus(status = 'NONE') { return CommentModel.find({status}); } @@ -155,7 +156,7 @@ module.exports = class CommentsService { * @param {String} asset_id * @return {Promise} */ - static moderationQueue(status = null, asset_id = null) { + static moderationQueue(status = 'NONE', asset_id = null) { // Fetch the comments with statuses. let comments = CommentModel.find({status}); diff --git a/test/services/comments.js b/test/services/comments.js index 4c3e0eab9..0bab40bfa 100644 --- a/test/services/comments.js +++ b/test/services/comments.js @@ -131,7 +131,7 @@ describe('services.CommentsService', () => { expect(c2).to.not.be.null; expect(c2.id).to.be.uuid; - expect(c2.status).to.be.null; + expect(c2.status).to.be.equal('NONE'); expect(c3).to.not.be.null; expect(c3.id).to.be.uuid; @@ -225,7 +225,7 @@ describe('services.CommentsService', () => { return CommentsService.findById(comment_id) .then((c) => { - expect(c.status).to.be.null; + expect(c.status).to.be.equal('NONE'); return CommentsService.pushStatus(comment_id, 'REJECTED', '123'); }) From 675f8238494b8afbe2dd39300435e4373ff8ab27 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 15 Feb 2017 17:32:54 -0300 Subject: [PATCH 23/35] no comments endpoints in /admin :tada: --- client/coral-admin/src/AppRouter.js | 2 - client/coral-admin/src/actions/comments.js | 103 ------------------ .../CommentStream/CommentStream.css | 13 --- .../containers/CommentStream/CommentStream.js | 66 ----------- .../src/graphql/queries/modQueueQuery.graphql | 1 - 5 files changed, 185 deletions(-) delete mode 100644 client/coral-admin/src/actions/comments.js delete mode 100644 client/coral-admin/src/containers/CommentStream/CommentStream.css delete mode 100644 client/coral-admin/src/containers/CommentStream/CommentStream.js diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index b28c4582b..bd72bbb8c 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -4,7 +4,6 @@ import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-ro import Streams from 'containers/Streams/Streams'; import Configure from 'containers/Configure/Configure'; import LayoutContainer from 'containers/LayoutContainer'; -import CommentStream from 'containers/CommentStream/CommentStream'; import InstallContainer from 'containers/Install/InstallContainer'; import CommunityContainer from 'containers/Community/CommunityContainer'; @@ -16,7 +15,6 @@ const routes = ( - diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js deleted file mode 100644 index 14f33bf36..000000000 --- a/client/coral-admin/src/actions/comments.js +++ /dev/null @@ -1,103 +0,0 @@ -import coralApi from '../../../coral-framework/helpers/response'; -import * as commentTypes from '../constants/comments'; -import * as actionTypes from '../constants/actions'; - -function addUsersCommentsActions (dispatch, {comments, users, actions}) { - dispatch({type: commentTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); - dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments}); - dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions}); -} - -// Get comments to fill each of the three lists on the mod queue -export const fetchModerationQueueComments = () => { - return dispatch => { - dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); - - return Promise.all([ - coralApi('/queue/comments/premod'), - coralApi('/queue/users/flagged'), - coralApi('/queue/comments/rejected'), - coralApi('/queue/comments/flagged') - ]) - .then(([premodComments, pendingUsers, rejected, flagged]) => { - - /* Combine seperate calls into a single object */ - flagged.comments.forEach(comment => comment.flagged = true); - return { - comments: [...premodComments.comments, ...rejected.comments, ...flagged.comments], - users: [...premodComments.users, ...pendingUsers.users, ...rejected.users, ...flagged.users], - actions: [...premodComments.actions, ...pendingUsers.actions, ...rejected.actions, ...flagged.actions] - }; - }) - .then(addUsersCommentsActions.bind(this, dispatch)); - }; -}; - -export const fetchPremodQueue = () => { - return dispatch => { - dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); - - return coralApi('/queue/comments/premod') - .then(addUsersCommentsActions.bind(this, dispatch)); - }; -}; - -export const fetchPendingUsersQueue = () => { - return dispatch => { - dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); - - return coralApi('/queue/users/flagged') - .then(addUsersCommentsActions.bind(this, dispatch)); - }; -}; - -export const fetchRejectedQueue = () => { - return dispatch => { - dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); - - return coralApi('/queue/comments/rejected') - .then(addUsersCommentsActions.bind(this, dispatch)); - }; -}; - -export const fetchFlaggedQueue = () => { - return dispatch => { - dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); - - return coralApi('/queue/comments/flagged') - .then(results => { - results.comments.forEach(comment => comment.flagged = true); - return results; - }) - .then(addUsersCommentsActions.bind(this, dispatch)); - }; -}; - -// Create a new comment -export const createComment = (name, body) => { - return (dispatch) => { - const formData = {body, name}; - return coralApi('/comments', {method: 'POST', body: formData}) - .then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res})) - .catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error})); - }; -}; - -/** - * Action disptacher related to comments - */ - -// Update a comment. Now to update a comment we need to send back the whole object -export const updateStatus = (status, comment) => { - return dispatch => { - dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_REQUEST, id: comment.id, status}); - return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}}) - .then(res => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_SUCCESS, res})) - .catch(error => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_FAILURE, error})); - }; -}; - -export const flagComment = id => (dispatch, getState) => { - dispatch({type: commentTypes.COMMENT_FLAG, id}); - dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)}); -}; diff --git a/client/coral-admin/src/containers/CommentStream/CommentStream.css b/client/coral-admin/src/containers/CommentStream/CommentStream.css deleted file mode 100644 index 9247183e5..000000000 --- a/client/coral-admin/src/containers/CommentStream/CommentStream.css +++ /dev/null @@ -1,13 +0,0 @@ - -@custom-media --big-viewport (min-width: 780px); - -.container { - max-width: 860px; - margin: 0 auto; -} - -@media (--big-viewport) { - .tab { - flex: none; - } -} diff --git a/client/coral-admin/src/containers/CommentStream/CommentStream.js b/client/coral-admin/src/containers/CommentStream/CommentStream.js deleted file mode 100644 index e2fac3702..000000000 --- a/client/coral-admin/src/containers/CommentStream/CommentStream.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import styles from './CommentStream.css'; -import {Snackbar} from 'react-mdl'; -import {connect} from 'react-redux'; -import {createComment, flagComment} from 'actions/comments'; -import ModerationList from 'components/ModerationList'; -import CommentBox from 'components/CommentBox'; - -/** - * Renders a comment stream using a ModerationList component - * and adds a box for adding a new comment - */ - -class CommentStream extends React.Component { - constructor (props) { - super(props); - this.state = {snackbar: false, snackbarMsg: ''}; - this.onSubmit = this.onSubmit.bind(this); - this.onClickAction = this.onClickAction.bind(this); - } - - // Fetch the comments before mounting - componentWillMount () { - this.props.dispatch({type: 'COMMENT_STREAM_FETCH'}); - } - - // Submit the new comment - onSubmit (comment) { - this.props.dispatch(createComment(comment.name, comment.body)); - } - - // The only action for now is flagging - onClickAction (action, id) { - if (action === 'flag') { - this.props.dispatch(flagComment(id)); - clearTimeout(this._snackTimeout); - this.setState({snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.'}); - this._snackTimeout = setTimeout(() => this.setState({snackbar: false}), 30000); - } - } - - // Render the comment box along with the ModerationList - render ({comments, users}, {snackbar, snackbarMsg}) { - return ( -
    - - - {snackbarMsg} -
    - ); - } -} - -const mapStateToProps = state => ({ - comments: state.comments.toJS(), - users: state.users.toJS() -}); - -export default connect(mapStateToProps)(CommentStream); diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql index 8f5bdd31f..ca7f7aa07 100644 --- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -9,7 +9,6 @@ query ModQueue ($asset_id: ID!) { } flagged: comments(query: { action_type: FLAG, - statuses: [null, PREMOD], asset_id: $asset_id }) { ...commentView From 1796efd3ba21a84cb61f48f3e0103a8800a28e5d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 15 Feb 2017 17:34:29 -0300 Subject: [PATCH 24/35] =?UTF-8?q?=C3=81dding=20NONE=20to=20the=20FE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-admin/src/graphql/queries/modQueueQuery.graphql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql index ca7f7aa07..735f3294e 100644 --- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -9,7 +9,8 @@ query ModQueue ($asset_id: ID!) { } flagged: comments(query: { action_type: FLAG, - asset_id: $asset_id + asset_id: $asset_id, + statuses: [NONE, PREMOD] }) { ...commentView action_summaries { From b26c170a52fe23327fec462dde0a24622c199d3c Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 15 Feb 2017 14:11:44 -0700 Subject: [PATCH 25/35] add some translations and link to mod queue --- .../coral-admin/src/components/FlagWidget.js | 37 ++++++++++++------- client/coral-admin/src/translations.json | 8 ++++ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index f01008e28..422087aaf 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -1,5 +1,10 @@ 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}) => { @@ -7,22 +12,28 @@ const FlagWidget = ({assets}) => { - - - + {/* empty on purpose */} + + + - {assets.map(asset => { - const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; - return ( - - - - - - ); - })} + { + assets.length + ? assets.map((asset, index) => { + const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; + return ( + + + + + + + ); + }) + : + }
    ArticleFlagsComment Count{lang.t('streams.article')}{lang.t('modqueue.flagged')}{lang.t('dashboard.comment_count')}
    {asset.title}{flagCount}{asset.commentCount}
    {index + 1}{lang.t('configure.moderate')} » {asset.title}{flagCount}{asset.commentCount}
    {lang.t('dashboard.no_flags')}
    ); diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 9c9f92868..2c55ead26 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -107,6 +107,10 @@ "email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.", "write_message": "Write a message" }, + "dashboard": { + "no_flags": "There have been no flags in the last 5 minutes! Hooray!", + "comment_count": "Comment Count" + }, "streams": { "search": "Search", "filter-streams": "Filter Streams", @@ -209,6 +213,10 @@ "cancel": "Cancelar", "yes_ban_user": "Si, Suspendan el usuario" }, + "dashbord": { + "no_flags": "", + "comment_count": "" + }, "streams": { "search": "", "filter-streams": "", From b3b36a245aa61ee65ed1a48d7ed0f658a53ce5b7 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 14:23:54 -0700 Subject: [PATCH 26/35] =?UTF-8?q?even=20more=20translations=20en=20Espa?= =?UTF-8?q?=C3=B1ol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-admin/src/translations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 2c55ead26..416353903 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -214,8 +214,8 @@ "yes_ban_user": "Si, Suspendan el usuario" }, "dashbord": { - "no_flags": "", - "comment_count": "" + "no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!", + "comment_count": "Cantidad de comentarios" }, "streams": { "search": "", From da1a931a403103f89ae705d18cb2bc15beaefdf1 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 15:13:54 -0700 Subject: [PATCH 27/35] remove a comma --- client/coral-plugin-questionbox/QuestionBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-plugin-questionbox/QuestionBox.js b/client/coral-plugin-questionbox/QuestionBox.js index ce9a51b9d..7ae680a09 100644 --- a/client/coral-plugin-questionbox/QuestionBox.js +++ b/client/coral-plugin-questionbox/QuestionBox.js @@ -3,7 +3,7 @@ const packagename = 'coral-plugin-questionbox'; const QuestionBox = ({enable, content}) =>
    + className={`${packagename}-info ${enable ? null : 'hidden'}` }> {content}
    ; From f91607cff9033456c259118d42e0e8bb59a9b85a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 15:36:46 -0700 Subject: [PATCH 28/35] show the author and publication date --- client/coral-admin/src/components/FlagWidget.css | 5 +++++ client/coral-admin/src/components/FlagWidget.js | 7 +++++-- client/coral-admin/src/graphql/queries/mostFlags.graphql | 2 ++ graph/typeDefs.graphql | 3 +++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.css b/client/coral-admin/src/components/FlagWidget.css index 03be150d4..f40da403d 100644 --- a/client/coral-admin/src/components/FlagWidget.css +++ b/client/coral-admin/src/components/FlagWidget.css @@ -27,3 +27,8 @@ .widgetTable tbody td { padding: 10px; } + +.lede { + font-size: 0.9em; + color: grey; +} diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index 422087aaf..aa8ceed30 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -25,8 +25,11 @@ const FlagWidget = ({assets}) => { const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; return ( - {index + 1} - {lang.t('configure.moderate')} » {asset.title} + {index + 1}. + + {asset.title} +

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

    + {flagCount} {asset.commentCount} diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index 1bcec7a1e..819f9126f 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -4,6 +4,8 @@ query Metrics ($from: Date!, $to: Date!) { title url commentCount + author + created_at action_summaries { actionCount actionableItemCount diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 5d9262612..a71985a36 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -339,6 +339,9 @@ type Asset { # The date that the asset was created. created_at: Date + + # The author(s) of the asset. + author: String } ################################################################################ From 93636aabac614262e976f425d5b93b2af0193f15 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 16:14:54 -0700 Subject: [PATCH 29/35] Added like summaries --- graph/loaders/metrics.js | 82 +++++++++++++++---------- graph/resolvers/asset_action_summary.js | 2 + graph/resolvers/root_query.js | 4 +- graph/typeDefs.graphql | 19 ++++-- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index a827ddeea..8b3dee5f5 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -3,7 +3,7 @@ const CommentModel = require('../../models/comment'); const AssetModel = require('../../models/asset'); const ActionModel = require('../../models/action'); -const getMetrics = (context, {from, to}) => { +const getMetrics = (context, {from, to, sort, limit}) => { let commentMetrics = {}; let assetMetrics = []; @@ -12,7 +12,6 @@ const getMetrics = (context, {from, to}) => { // Find all actions that were created in the time range. {$match: { - action_type: 'FLAG', item_type: 'COMMENTS', created_at: { $gt: from, @@ -22,7 +21,10 @@ const getMetrics = (context, {from, to}) => { // Count all those items. {$group: { - _id: '$item_id', + _id: { + item_id: '$item_id', + action_type: '$action_type' + }, count: { $sum: 1 } @@ -30,18 +32,23 @@ const getMetrics = (context, {from, to}) => { // Project the count to a better field. {$project: { - item_id: '$_id', + item_id: '$_id.item_id', + action_type: '$_id.action_type', count: '$count' }} ]).then((actionSummaries) => { // Collect all the action summaries into a dictionary. - actionSummaries.forEach((actionSummary) => { - commentMetrics[actionSummary.item_id] = actionSummary.count; + actionSummaries.forEach(({item_id, action_type, count}) => { + if (!(item_id in commentMetrics)) { + commentMetrics[item_id] = []; + } + + commentMetrics[item_id].push({action_type, count}); }); // Collect just the comment id's. - let commentIDs = actionSummaries.map((as) => as.item_id); + let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); // Find those comments. return CommentModel.aggregate([ @@ -72,29 +79,46 @@ const getMetrics = (context, {from, to}) => { }) .then((commentResults) => { - // Compute all the action summaries for the assets based on the time slice - // that you requested. - commentResults.forEach((result) => { - let actionCount = 0; + assetMetrics = commentResults.map(({ids, asset_id}) => { + let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type'); - result.ids.forEach((id) => { - actionCount += commentMetrics[id]; - }); + 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 + })); - assetMetrics.push({ - id: result.asset_id, - actionCount, - actionableItemCount: result.ids.length - }); + return {action_summaries, id: asset_id}; }); - // Sort the assets by flag count. + // 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) => { - return b.flags - a.flags; + 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; }); - // Only keep the top 10. - assetMetrics = assetMetrics.slice(0, 10); + // Only keep the top `limit`. + assetMetrics = assetMetrics.slice(0, limit); // Determine the assets that we need to return. return AssetModel.find({ @@ -109,18 +133,12 @@ const getMetrics = (context, {from, to}) => { let groupedAssets = _.groupBy(assets, 'id'); // Return from the sorted asset metrics and return their assetes. - return assetMetrics.map(({id, actionCount, actionableItemCount}) => { + return assetMetrics.map(({id, action_summaries}) => { if (id in groupedAssets) { let asset = groupedAssets[id][0]; - let flagAssetActionSummary = { - action_type: 'FLAG', - actionCount, - actionableItemCount - }; - // Add the action summaries to the asset. - asset.action_summaries = [flagAssetActionSummary]; + asset.action_summaries = action_summaries; return asset; } @@ -132,6 +150,6 @@ const getMetrics = (context, {from, to}) => { module.exports = (context) => ({ Metrics: { - get: ({from, to}) => getMetrics(context, {from, to}) + get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit}) } }); diff --git a/graph/resolvers/asset_action_summary.js b/graph/resolvers/asset_action_summary.js index 99483f9bb..c4a3cef01 100644 --- a/graph/resolvers/asset_action_summary.js +++ b/graph/resolvers/asset_action_summary.js @@ -3,6 +3,8 @@ const AssetActionSummary = { switch (action_type) { case 'FLAG': return 'FlagAssetActionSummary'; + case 'LIKE': + return 'LikeAssetActionSummary'; } } }; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 432ad126f..eb66274dd 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -39,12 +39,12 @@ const RootQuery = { return Comments.getByQuery(query); }, - metrics(_, {from, to}, {user, loaders: {Metrics}}) { + metrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics}}) { if (user == null || !user.hasRoles('ADMIN')) { return null; } - return Metrics.get({from, to}); + return Metrics.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 a71985a36..e14b7676f 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -201,10 +201,20 @@ interface AssetActionSummary { # A summary of counts related to all the Flags on an Asset. type FlagAssetActionSummary implements AssetActionSummary { - # Number of actions associated with actionable types on this this Asset. + # Number of flags associated with actionable types on this this Asset. actionCount: Int - # Number of unique actionable types that are referenced by the actions. + # Number of unique actionable types that are referenced by the flags. + actionableItemCount: Int +} + +# A summary of counts related to all the Likes on an Asset. +type LikeAssetActionSummary implements AssetActionSummary { + + # Number of likes associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the likes. actionableItemCount: Int } @@ -414,8 +424,9 @@ type RootQuery { # The currently logged in user based on the request. me: User - # Metrics related to user actions are saturated into the assets returned. - metrics(from: Date!, to: Date!): [Asset] + # 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] } ################################################################################ From 6c141cbce06b629b6b01c0c12fbe50774d962b3a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 16:52:06 -0700 Subject: [PATCH 30/35] now with likes --- client/coral-admin/src/components/FlagWidget.js | 3 +++ client/coral-admin/src/graphql/queries/index.js | 3 ++- client/coral-admin/src/graphql/queries/mostFlags.graphql | 4 ++-- client/coral-admin/src/translations.json | 6 ++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index aa8ceed30..8116d4ff9 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -15,6 +15,7 @@ const FlagWidget = ({assets}) => { {/* empty on purpose */} {lang.t('streams.article')} {lang.t('modqueue.flagged')} + {lang.t('modqueue.likes')} {lang.t('dashboard.comment_count')} @@ -23,6 +24,7 @@ const FlagWidget = ({assets}) => { assets.length ? assets.map((asset, index) => { const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; + const likeCount = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary').actionCount; return ( {index + 1}. @@ -30,6 +32,7 @@ const FlagWidget = ({assets}) => { {asset.title}

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

    + {likeCount} {flagCount} {asset.commentCount} diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index 9b8a51216..5a429756e 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -8,9 +8,10 @@ export const mostFlags = graphql(MOST_FLAGS, { // currently hard-coded per Greg's advice const fiveMinutesAgo = new Date(); - fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5); + fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 305); return { variables: { + sort: 'FLAG', from: fiveMinutesAgo.toISOString(), to: new Date().toISOString() } diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index 819f9126f..182ea0c8b 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -1,5 +1,5 @@ -query Metrics ($from: Date!, $to: Date!) { - metrics(from: $from, to: $to) { +query Metrics ($from: Date!, $to: Date!, $sort: ACTION_TYPE!) { + metrics(from: $from, to: $to, sort: $sort) { id title url diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 416353903..5b15564d7 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -16,6 +16,7 @@ "loading": "Loading results" }, "modqueue": { + "likes": "likes", "all": "all", "premod": "pre-mod", "rejected": "rejected", @@ -109,7 +110,7 @@ }, "dashboard": { "no_flags": "There have been no flags in the last 5 minutes! Hooray!", - "comment_count": "Comment Count" + "comment_count": "Comments" }, "streams": { "search": "Search", @@ -145,6 +146,7 @@ "loading": "Cargando resultados" }, "modqueue": { + "likes": "gustos", "premod": "pre-mod", "rejected": "rechazado", "flagged": "marcado", @@ -215,7 +217,7 @@ }, "dashbord": { "no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!", - "comment_count": "Cantidad de comentarios" + "comment_count": "Comentarios" }, "streams": { "search": "", From 9eefc0299f104812adae9c443eb8e55a830a6989 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:11:50 -0700 Subject: [PATCH 31/35] Optimization pass --- graph/loaders/metrics.js | 228 +++++++++++++++++++++------------------ graph/loaders/util.js | 10 ++ 2 files changed, 133 insertions(+), 105 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index 8b3dee5f5..ea3fe4d31 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -1,13 +1,104 @@ const _ = require('lodash'); +const DataLoader = require('dataloader'); +const util = require('./util'); +const objectCacheKeyFn = util.objectCacheKeyFn; +const arrayCacheKeyFn = util.arrayCacheKeyFn; + const CommentModel = require('../../models/comment'); -const AssetModel = require('../../models/asset'); const ActionModel = require('../../models/action'); -const getMetrics = (context, {from, to, sort, limit}) => { +const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => { let commentMetrics = {}; let assetMetrics = []; + return Metrics.getRecentActions.load({from, to}) + .then((actionSummaries) => { + + commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => { + 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 = _.uniq(actionSummaries.map((as) => as.item_id)); + + // Find those comments. + return Metrics.getSpecificComments.load(commentIDs); + }) + .then((commentResults) => { + + assetMetrics = commentResults.map(({ids, asset_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}; + }); + + // 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) => { + 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; + }); + + // Only keep the top `limit`. + assetMetrics = assetMetrics.slice(0, limit); + + // Determine the assets that we need to return. + return Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id)); + }) + .then((assets) => { + + // 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); + }); +}; + +const getRecentActions = (context, {from, to}) => { return ActionModel.aggregate([ // Find all actions that were created in the time range. @@ -36,120 +127,47 @@ const getMetrics = (context, {from, to, sort, limit}) => { action_type: '$_id.action_type', count: '$count' }} - ]).then((actionSummaries) => { + ]); +}; - // Collect all the action summaries into a dictionary. - actionSummaries.forEach(({item_id, action_type, count}) => { - if (!(item_id in commentMetrics)) { - commentMetrics[item_id] = []; - } +const getSpecificComments = (context, ids) => { + return CommentModel.aggregate([ - commentMetrics[item_id].push({action_type, count}); - }); - - // Collect just the comment id's. - let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); - - // Find those comments. - return CommentModel.aggregate([ - - // Get only those comments. - {$match: { - id: { - $in: commentIDs - } - }}, - - // Group by their asset id and push in the comment id. - {$group: { - _id: { - asset_id: '$asset_id' - }, - ids: { - $addToSet: '$id' - } - }}, - - // Project that data only as better fields. - {$project: { - asset_id: '$_id.asset_id', - ids: '$ids' - }} - ]); - }) - .then((commentResults) => { - - assetMetrics = commentResults.map(({ids, asset_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}; - }); - - // 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) => { - 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; - }); - - // Only keep the top `limit`. - assetMetrics = assetMetrics.slice(0, limit); - - // Determine the assets that we need to return. - return AssetModel.find({ + // Get only those comments. + {$match: { id: { - $in: assetMetrics.map((asset) => asset.id) + $in: ids } - }); - }) - .then((assets) => { + }}, - // 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; + // Group by their asset id and push in the comment id. + {$group: { + _id: { + asset_id: '$asset_id' + }, + ids: { + $addToSet: '$id' } + }}, - return null; - }).filter((asset) => asset != null); - }); + // Project that data only as better fields. + {$project: { + asset_id: '$_id.asset_id', + ids: '$ids' + }} + ]); }; module.exports = (context) => ({ Metrics: { + getSpecificComments: new DataLoader(([ids]) => getSpecificComments(context, ids).then((c) => [c]), { + batch: false, + cacheKeyFn: arrayCacheKeyFn + }), + 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}) } }); diff --git a/graph/loaders/util.js b/graph/loaders/util.js index 4640d8245..ab6f8a1f3 100644 --- a/graph/loaders/util.js +++ b/graph/loaders/util.js @@ -130,10 +130,20 @@ const objectCacheKeyFn = (...paths) => (obj) => { return paths.map((path) => obj[path]).join(':'); }; +/** + * Maps an object's paths to a string that can be used as a cache key. + * @param {Array} paths paths on the object to be used to generate the cache + * key + */ +const arrayCacheKeyFn = (arr) => { + return arr.sort().join(':'); +}; + module.exports = { singleJoinBy, arrayJoinBy, objectCacheKeyFn, + arrayCacheKeyFn, SingletonResolver, SharedCacheDataLoader }; From b75aed8bd8bacd6faa886423a7d7612c9f7420d3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:28:47 -0700 Subject: [PATCH 32/35] Another optimization pass --- graph/loaders/metrics.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index ea3fe4d31..a6170f912 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -1,8 +1,6 @@ const _ = require('lodash'); const DataLoader = require('dataloader'); -const util = require('./util'); -const objectCacheKeyFn = util.objectCacheKeyFn; -const arrayCacheKeyFn = util.arrayCacheKeyFn; +const {objectCacheKeyFn} = require('./util'); const CommentModel = require('../../models/comment'); const ActionModel = require('../../models/action'); @@ -29,7 +27,7 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => { let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); // Find those comments. - return Metrics.getSpecificComments.load(commentIDs); + return Metrics.getSpecificComments.loadMany(commentIDs); }) .then((commentResults) => { @@ -160,10 +158,7 @@ const getSpecificComments = (context, ids) => { module.exports = (context) => ({ Metrics: { - getSpecificComments: new DataLoader(([ids]) => getSpecificComments(context, ids).then((c) => [c]), { - batch: false, - cacheKeyFn: arrayCacheKeyFn - }), + 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') From 27819d0d6a55d50d05ddabb7eb532cacbd162c8d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:42:54 -0700 Subject: [PATCH 33/35] Changed aggregate -> find --- graph/loaders/metrics.js | 41 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index a6170f912..a842165c0 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -29,9 +29,12 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => { // Find those comments. return Metrics.getSpecificComments.loadMany(commentIDs); }) - .then((commentResults) => { + .then((comments) => { - assetMetrics = commentResults.map(({ids, asset_id}) => { + let commentResults = _.groupBy(comments, 'asset_id'); + + 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) => ({ @@ -129,31 +132,15 @@ const getRecentActions = (context, {from, to}) => { }; const getSpecificComments = (context, ids) => { - return CommentModel.aggregate([ - - // Get only those comments. - {$match: { - id: { - $in: ids - } - }}, - - // Group by their asset id and push in the comment id. - {$group: { - _id: { - asset_id: '$asset_id' - }, - ids: { - $addToSet: '$id' - } - }}, - - // Project that data only as better fields. - {$project: { - asset_id: '$_id.asset_id', - ids: '$ids' - }} - ]); + return CommentModel.find({ + id: { + $in: ids + } + }) + .select({ + id: 1, + asset_id: 1 + }); }; module.exports = (context) => ({ From fa152bda3f021af28a3741bc73d28748eaec7b1c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:48:40 -0700 Subject: [PATCH 34/35] Added count clearning for status changes --- graph/mutators/comment.js | 20 +++++++++++++++++--- services/comments.js | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 91b2b0d01..a56612b83 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -169,9 +169,23 @@ const createPublicComment = (context, commentInput) => { * @param {String} status the new status of the comment */ -const setCommentStatus = ({comment}, {id, status}) => { - return CommentsService.setStatus(id, status) - .then(res => res); +const setCommentStatus = ({loaders: {Comments}}, {id, status}) => { + return CommentsService + .setStatus(id, status) + .then((comment) => { + + // If the loaders are present, clear the caches for these values because we + // just added a new comment, hence the counts should be updated. + if (Comments && Comments.countByAssetID && Comments.countByParentID) { + if (comment.parent_id != null) { + Comments.countByParentID.clear(comment.parent_id); + } else { + Comments.countByAssetID.clear(comment.asset_id); + } + } + + return comment; + }); }; module.exports = (context) => { diff --git a/services/comments.js b/services/comments.js index 8ec2b6038..ad9ce18db 100644 --- a/services/comments.js +++ b/services/comments.js @@ -273,7 +273,7 @@ module.exports = class CommentsService { return Promise.reject(new Error(`status ${status} is not supported`)); } - return CommentModel.update({id}, { + return CommentModel.findOneAndUpdate({id}, { $set: {status} }); } From 3be8ae39c4265347d0e8e8d89f35cbbf57a8917b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:53:13 -0700 Subject: [PATCH 35/35] Removed old TODO's --- graph/mutators/action.js | 4 ---- graph/mutators/comment.js | 4 ---- graph/mutators/user.js | 4 ---- 3 files changed, 12 deletions(-) diff --git a/graph/mutators/action.js b/graph/mutators/action.js index 3499719e1..a572a641c 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -45,10 +45,6 @@ const deleteAction = ({user}, {id}) => { }; module.exports = (context) => { - - // TODO: refactor to something that'll return an error in the event an attempt - // is made to mutate state while not logged in. There's got to be a better way - // to do this. if (context.user && context.user.can('mutation:createAction', 'mutation:deleteAction')) { return { Action: { diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index a56612b83..5007a3039 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -189,10 +189,6 @@ const setCommentStatus = ({loaders: {Comments}}, {id, status}) => { }; module.exports = (context) => { - - // TODO: refactor to something that'll return an error in the event an attempt - // is made to mutate state while not logged in. There's got to be a better way - // to do this. let mutators = { Comment: { create: () => Promise.reject(errors.ErrNotAuthorized), diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 2c43f11be..3f87c1fb5 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -7,10 +7,6 @@ const setUserStatus = ({user}, {id, status}) => { }; module.exports = (context) => { - - // TODO: refactor to something that'll return an error in the event an attempt - // is made to mutate state while not logged in. There's got to be a better way - // to do this. if (context.user && context.user.can('mutation:setUserStatus')) { return { User: {