diff --git a/.gitignore b/.gitignore index d30e961bd..f47efe570 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ plugins/* !plugins/talk-plugin-slack-notifications **/node_modules/* +yarn-error.log diff --git a/.nodemon.json b/.nodemon.json index 7f7fd3d59..36fed8700 100644 --- a/.nodemon.json +++ b/.nodemon.json @@ -1,5 +1,11 @@ { + "exec": "npm-run-all --parallel generate-introspection start:development", "verbose": true, "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"], - "ext": "js,json,graphql" + "ext": "js,json,graphql", + "watch": [ + ".", + "bin/cli", + "bin/cli-serve" + ] } diff --git a/bin/cli-users b/bin/cli-users index 89f53a67d..b82ee561d 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -106,20 +106,17 @@ async function createUser(options) { } const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim()); - console.log(`Created user ${user.id}.`); if (answers.roles.length > 0) { - return Promise.all(answers.roles.map((role) => { - return UsersService - .addRoleToUser(user.id, role) - .then(() => { - console.log(`Added the role ${role} to User ${user.id}.`); - }); - })); + for (const role of answers.roles) { + await UsersService.addRoleToUser(user.id, role); + } } - util.shutdown(); + await UsersService.sendEmailConfirmation(user, answers.email.trim()); + console.log(`Created User ${user.id}.`); + util.shutdown(); } catch (err) { console.error(err); util.shutdown(); @@ -241,12 +238,12 @@ function listUsers() { }); users.forEach((user) => { - let state = user.disabled ? 'Disabled' : 'Enabled'; const profile = user.profiles.find(({provider}) => provider === 'local'); + let state; if (profile && profile.metadata && profile.metadata.confirmed_at) { - state += ', Verified'; + state = 'Verified'; } else { - state += ', Unverified'; + state = 'Unverified'; } table.push([ @@ -336,74 +333,6 @@ function removeRole(userID, role) { }); } -/** - * Ban a user - * @param {String} userID id of the user to ban - */ -function ban(userID) { - UsersService - .setStatus(userID, 'BANNED') - .then(() => { - console.log(`Banned the User ${userID}.`); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); -} - -/** - * Unban a user - * @param {String} userUD id of the user to remove the role from - */ -function unban(userID) { - UsersService - .setStatus(userID, 'ACTIVE') - .then(() => { - console.log(`Unban the User ${userID}.`); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); -} - -/** - * Disable a given user. - * @param {String} userID the ID of a user to disable - */ -function disableUser(userID) { - UsersService - .disableUser(userID) - .then(() => { - console.log(`User ${userID} was disabled.`); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); -} - -/** - * Enabled a given user. - * @param {String} userID the ID of a user to enable - */ -function enableUser(userID) { - UsersService - .enableUser(userID) - .then(() => { - console.log(`User ${userID} was enabled.`); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); -} - /** * Verifies an email address for a user. * @@ -472,26 +401,6 @@ program .description('removes a role from a given user') .action(removeRole); -program - .command('ban ') - .description('ban a given user') - .action(ban); - -program - .command('uban ') - .description('unban a given user') - .action(unban); - -program - .command('disable ') - .description('disable a given user from logging in') - .action(disableUser); - -program - .command('enable ') - .description('enable a given user from logging in') - .action(enableUser); - program .command('verify ') .description('verifies the given user\'s email address') diff --git a/bin/verifications/database/action_counts.js b/bin/verifications/database/action_counts.js new file mode 100644 index 000000000..fff3a0569 --- /dev/null +++ b/bin/verifications/database/action_counts.js @@ -0,0 +1,185 @@ +const UserModel = require('../../../models/user'); +const CommentModel = require('../../../models/comment'); +const ActionsService = require('../../../services/actions'); +const {arrayJoinBy} = require('../../../graph/loaders/util'); +const sc = require('snake-case'); +const debug = require('debug')('talk:cli:verify'); + +const MODELS = [ + UserModel, + CommentModel, +]; + +async function processBatch(Model, documents) { + + // Get an array of all the document id's. + const documentIDs = documents.map(({id}) => id); + + // Store all the operations on this batch in this array that we'll return + // later. + const operations = []; + + // Get the action summaries for this batch. + const totalActionSummaries = await ActionsService + .getActionSummaries(documentIDs) + .then(arrayJoinBy(documentIDs, 'item_id')); + + // Iterate over the documents. + for (let i = 0; i < documents.length; i++) { + const document = documents[i]; + const actionSummaries = totalActionSummaries[i]; + + let ops = []; + + for (const actionSummary of actionSummaries) { + if (actionSummary.group_id === null) { + continue; + } + + // And we generate the group id. + const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); + const GROUP_ID = sc(actionSummary.group_id.toLowerCase()); + + if (GROUP_ID.length <= 0) { + continue; + } + + // And we add a new batch operation if the action summary is associated + // with a group. + const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`; + + // Check that the action summaries match the cached counts. + if ( + !document.action_counts || + !(ACTION_COUNT_FIELD in document.action_counts) || + document.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count + ) { + + // Batch updates for those changes. + ops.push({ + [`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count, + }); + } + } + + // Group all the action summaries together from all the different group + // ids. + let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => { + const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); + + if (!(ACTION_TYPE in acc)) { + acc[ACTION_TYPE] = 0; + } + + acc[ACTION_TYPE] += actionSummary.count; + + return acc; + }, {}); + + for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) { + const count = groupedActionSummaries[ACTION_COUNT_FIELD]; + + // Check that the action summaries match the cached counts. + if ( + !document.action_counts || + !(ACTION_COUNT_FIELD in document.action_counts) || + document.action_counts[ACTION_COUNT_FIELD] !== count + ) { + + // Batch updates for those changes. + ops.push({ + [`action_counts.${ACTION_COUNT_FIELD}`]: count, + }); + } + } + + // If this comment has action summaries that should be updated, then + // perform an update! + if (ops.length > 0) { + operations.push({ + updateOne: { + filter: { + id: document.id + }, + update: { + $set: Object.assign({}, ...ops), + }, + }, + }); + } + } + + return operations; +} + +module.exports = async ({fix, batch}) => { + for (const Model of MODELS) { + const cursor = Model + .collection + .find({}) + .project({ + id: 1, + action_counts: 1 + }) + .sort({created_at: 1}); + + let operations = []; + let documents = []; + + // While there are documents to process. + while (await cursor.hasNext()) { + + // Load the document. + const document = await cursor.next(); + + // Push the document into the documents array. + documents.push(document); + + // Check to see if the length of the documents array requires us to + // process it. + if (documents.length > batch) { + + // Process this batch. + let batchOperations = await processBatch(Model, documents); + + // Push the batch operations into the model operations. + operations.push(...batchOperations); + + // Clear this batch contents. + documents = []; + } + } + + // Check to see if there are any documents left over. + if (documents.length > 0) { + + // Process this batch. + let batchOperations = await processBatch(Model, documents); + + // Push the batch operations into the model operations. + operations.push(...batchOperations); + } + + const OPERATIONS_LENGTH = operations.length; + + console.log(`action_counts.js: ${OPERATIONS_LENGTH} ${Model.collection.name} need their action counts fixed.`); + + // If fix was enabled, execute the batch writes. + if (OPERATIONS_LENGTH > 0) { + if (fix) { + debug(`action_counts.js: fixing ${OPERATIONS_LENGTH} ${Model.collection.name}...`); + + while (operations.length) { + let result = await Model.collection.bulkWrite(operations.splice(0, batch)); + + debug(`action_counts.js: fixed batch of ${result.modifiedCount} ${Model.collection.name}.`); + } + + console.log(`action_counts.js: applied all ${OPERATIONS_LENGTH} fixes to ${Model.collection.name}.`); + } else { + console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors'); + } + } + } +}; + diff --git a/bin/verifications/database/comments.js b/bin/verifications/database/comment_replies.js similarity index 61% rename from bin/verifications/database/comments.js rename to bin/verifications/database/comment_replies.js index 231badabb..03a3a4ac6 100644 --- a/bin/verifications/database/comments.js +++ b/bin/verifications/database/comment_replies.js @@ -1,7 +1,5 @@ const CommentModel = require('../../../models/comment'); -const ActionsService = require('../../../services/actions'); -const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util'); -const sc = require('snake-case'); +const {singleJoinBy} = require('../../../graph/loaders/util'); const debug = require('debug')('talk:cli:verify'); const getBatch = async (limit, offset) => CommentModel @@ -55,15 +53,9 @@ module.exports = async ({fix, limit, batch}) => { .then(singleJoinBy(commentIDs, '_id')) .then((results) => results.map((result) => result ? result.count : 0)); - // Get their action summaries. - let allActionSummaries = await ActionsService - .getActionSummaries(commentIDs) - .then(arrayJoinBy(commentIDs, 'item_id')); - // Loop over the comments, with their action summaries. for (let i = 0; i < comments.length; i++) { let comment = comments[i]; - let actionSummaries = allActionSummaries[i]; let replyCount = allReplyCounts[i]; // And check to see if the action summaries we just computed match what is @@ -77,61 +69,6 @@ module.exports = async ({fix, limit, batch}) => { }); } - // First we process all the group id's. - for (let actionSummary of actionSummaries) { - if (actionSummary.group_id === null) { - continue; - } - - // And we generate the group id. - const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); - const GROUP_ID = sc(actionSummary.group_id.toLowerCase()); - - if (GROUP_ID.length <= 0) { - continue; - } - - // And we add a new batch operation if the action summary is associated - // with a group. - const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`; - - // Check that the action summaries match the cached counts. - if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) { - - // Batch updates for those changes. - commentOperations.push({ - [`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count, - }); - } - } - - // Group all the action summaries together from all the different group - // ids. - let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => { - const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); - - if (!(ACTION_TYPE in acc)) { - acc[ACTION_TYPE] = 0; - } - - acc[ACTION_TYPE] += actionSummary.count; - - return acc; - }, {}); - - for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) { - const count = groupedActionSummaries[ACTION_COUNT_FIELD]; - - // Check that the action summaries match the cached counts. - if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) { - - // Batch updates for those changes. - commentOperations.push({ - [`action_counts.${ACTION_COUNT_FIELD}`]: count, - }); - } - } - // If this comment has action summaries that should be updated, then // perform an update! if (commentOperations.length > 0) { diff --git a/bin/verifications/database/index.js b/bin/verifications/database/index.js index 3dafeb3bf..2a599cfac 100644 --- a/bin/verifications/database/index.js +++ b/bin/verifications/database/index.js @@ -6,7 +6,8 @@ // // async ({fix = false, batch = 1000}) => {} // -// where their options are derrived. +// where their options are derived. module.exports = [ - require('./comments'), + require('./comment_replies'), + require('./action_counts'), ]; diff --git a/circle.yml b/circle.yml index 9f35736c6..ebb746669 100644 --- a/circle.yml +++ b/circle.yml @@ -7,6 +7,8 @@ machine: environment: PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" NODE_ENV: "test" + MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml" + MOCHA_REPORTER: "mocha-junit-reporter" pre: # TODO: use the following to add in support for MongoDB 3.4. # # Upgrade the database version to 3.4. @@ -47,10 +49,11 @@ database: test: override: # Run the tests using the junit reporter. - - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test + - yarn test + # Run the end to end tests + - yarn e2e:ci # Check dependancies using nsp. - nsp check - - yarn e2e-ci deployment: release: diff --git a/client/coral-admin/src/containers/Header.js b/client/coral-admin/src/containers/Header.js index a5c1f0afc..e2c3692c4 100644 --- a/client/coral-admin/src/containers/Header.js +++ b/client/coral-admin/src/containers/Header.js @@ -14,7 +14,7 @@ export default withQuery(gql` }) flaggedUsernamesCount: userCount(query: { action_type: FLAG, - statuses: [PENDING] + statuses: [SET, CHANGED] }) } `, { diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index 4cf9eeb8a..6cb5e700d 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -22,7 +22,7 @@ const withData = withQuery(gql` query TalkAdmin_Community { flaggedUsernamesCount: userCount(query: { action_type: FLAG, - statuses: [PENDING] + statuses: [SET, CHANGED] }) ...${getDefinitionName(FlaggedAccounts.fragments.root)} ...${getDefinitionName(FlaggedUser.fragments.root)} diff --git a/client/coral-configure/components/CloseCommentsInfo.js b/client/coral-configure/components/CloseCommentsInfo.js index d10419f3f..cc0e12a77 100644 --- a/client/coral-configure/components/CloseCommentsInfo.js +++ b/client/coral-configure/components/CloseCommentsInfo.js @@ -26,4 +26,4 @@ CloseCommentsInfo.propTypes = { onClick: PropTypes.func, }; -export default CloseCommentsInfo; \ No newline at end of file +export default CloseCommentsInfo; diff --git a/client/coral-settings/components/Bio.css b/client/coral-settings/components/Bio.css deleted file mode 100644 index 8c36b989d..000000000 --- a/client/coral-settings/components/Bio.css +++ /dev/null @@ -1,21 +0,0 @@ -.bio textarea { - width: 100%; - box-sizing: border-box; - border-radius: 2px; - min-height: 100px; - margin: 10px 0; - border: solid 1px #d8d8d8; -} - -.bio h1 { - font-size: 16px; - margin: 3px 0; -} - -.bio p { - margin: 3px 0; -} - -.actions { - text-align: right; -} diff --git a/client/coral-settings/components/Bio.js b/client/coral-settings/components/Bio.js deleted file mode 100644 index cd1347781..000000000 --- a/client/coral-settings/components/Bio.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import styles from './Bio.css'; -import {Button} from '../../coral-ui'; - -export default ({bio, handleSave, handleInput, handleCancel}) => ( -
-

Bio

-

Tell the community about yourself

-
-