From e3cdad1f2aaa3a1da1523dc8b48796c0bca071f8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 16 Jan 2018 16:47:05 -0700 Subject: [PATCH 1/5] added asset re-write --- bin/cli-assets | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ models/asset.js | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/bin/cli-assets b/bin/cli-assets index d14df320b..e13d7db28 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -14,6 +14,8 @@ const AssetsService = require('../services/assets'); const mongoose = require('../services/mongoose'); const scraper = require('../services/scraper'); const inquirer = require('inquirer'); +const { URL } = require('url'); +const errors = require('../errors'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); @@ -125,6 +127,51 @@ async function merge(srcID, dstID) { } } +async function rewrite(search, replace) { + try { + search = new RegExp(search); + + const assets = await AssetModel.find({ + url: { $regex: search }, + }); + if (assets.length === 0) { + console.log(`No assets found with the pattern: ${search}`); + return util.shutdown(0); + } + + const bulk = AssetModel.collection.initializeUnorderedBulkOp(); + + let ops = 0; + assets.forEach(({ id, url: oldURL }) => { + // Replace the url. + const newURL = oldURL.replace(search, replace); + + // Try to validate that the new url is valid. + try { + new URL(newURL); + } catch (err) { + throw errors.ErrInvalidAssetURL; + } + + // If the url was updated with the operation, then queue up the update op. + if (newURL !== oldURL) { + ops++; + bulk.find({ id }).updateOne({ $set: { url: newURL } }); + } + }); + + if (ops > 0) { + await bulk.execute(); + } + console.log(`${ops} assets had their url's updated`); + + util.shutdown(0); + } catch (err) { + console.error(err); + util.shutdown(1); + } +} + //============================================================================== // Setting up the program command line arguments. //============================================================================== @@ -151,6 +198,13 @@ program ) .action(merge); +program + .command('rewrite ') + .description( + "rewrites asset url's using the provided regex replacement pattern" + ) + .action(rewrite); + program.parse(process.argv); // If there is no command listed, output help. diff --git a/models/asset.js b/models/asset.js index 1f565a110..68ca08bbf 100644 --- a/models/asset.js +++ b/models/asset.js @@ -41,7 +41,7 @@ const AssetSchema = new Schema( publication_date: Date, modified_date: Date, - // This object is used exclusivly for storing settings that are to override + // This object is used exclusively for storing settings that are to override // the base settings from the base Settings object. This is to be accessed // always after running `rectifySettings` against it. settings: { From 45d6800143517003ed0b127935b15857678a8a9b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 16 Jan 2018 17:10:59 -0700 Subject: [PATCH 2/5] added dry mode --- bin/cli-assets | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/bin/cli-assets b/bin/cli-assets index e13d7db28..78229d91b 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -15,7 +15,6 @@ const mongoose = require('../services/mongoose'); const scraper = require('../services/scraper'); const inquirer = require('inquirer'); const { URL } = require('url'); -const errors = require('../errors'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); @@ -127,7 +126,7 @@ async function merge(srcID, dstID) { } } -async function rewrite(search, replace) { +async function rewrite(search, replace, options) { try { search = new RegExp(search); @@ -139,9 +138,7 @@ async function rewrite(search, replace) { return util.shutdown(0); } - const bulk = AssetModel.collection.initializeUnorderedBulkOp(); - - let ops = 0; + let opts = []; assets.forEach(({ id, url: oldURL }) => { // Replace the url. const newURL = oldURL.replace(search, replace); @@ -150,20 +147,41 @@ async function rewrite(search, replace) { try { new URL(newURL); } catch (err) { - throw errors.ErrInvalidAssetURL; + throw new Error( + `Rewrite would have replaced the valid URL ${oldURL} with an invalid one ${newURL}` + ); } - // If the url was updated with the operation, then queue up the update op. - if (newURL !== oldURL) { - ops++; - bulk.find({ id }).updateOne({ $set: { url: newURL } }); - } + opts.push({ + find: { id }, + updateOne: { $set: { url: newURL } }, + id, + oldURL, + newURL, + }); }); - if (ops > 0) { - await bulk.execute(); + if (opts.length > 0) { + if (options.dryRun) { + const table = new Table({ head: ['ID', 'Old URL', 'New URL'] }); + + opts.forEach(({ id, oldURL, newURL }) => { + table.push([id, oldURL, newURL]); + }); + + console.log(table.toString()); + } else { + const bulk = AssetModel.collection.initializeUnorderedBulkOp(); + opts.forEach(({ find, updateOne, oldURL, newURL }) => { + // If the url was updated with the operation, then queue up the update op. + if (newURL !== oldURL) { + bulk.find(find).updateOne(updateOne); + } + }); + await bulk.execute(); + console.log(`${opts.length} assets had their url's updated`); + } } - console.log(`${ops} assets had their url's updated`); util.shutdown(0); } catch (err) { @@ -200,6 +218,7 @@ program program .command('rewrite ') + .option('-d, --dry-run', 'enables dry run of the replacement') .description( "rewrites asset url's using the provided regex replacement pattern" ) From 3938b7883fe2b532d812ec0aa79043bd76afcaa5 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 17 Jan 2018 13:24:54 -0700 Subject: [PATCH 3/5] introduced some locale fixes --- client/coral-framework/services/bootstrap.js | 6 +++- client/coral-framework/services/i18n.js | 35 ++++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index b1014f2db..912c6c532 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -5,7 +5,7 @@ import EventEmitter from 'eventemitter2'; import { createReduxEmitter } from './events'; import { createRestClient } from './rest'; import thunk from 'redux-thunk'; -import { loadTranslations } from './i18n'; +import { setupTranslations, loadTranslations } from './i18n'; import bowser from 'bowser'; import noop from 'lodash/noop'; import { BASE_PATH } from 'coral-framework/constants/url'; @@ -88,6 +88,10 @@ export async function createContext({ const pymStorage = createPymStorage(pym); const history = createHistory(BASE_PATH); const introspection = createIntrospection(introspectionData); + + // Setup the translation framework with the storage. + setupTranslations(storage); + let store = null; const token = () => { // Try to get the token from localStorage. If it isn't here, it may diff --git a/client/coral-framework/services/i18n.js b/client/coral-framework/services/i18n.js index 6c4aa00a9..4c387a0ca 100644 --- a/client/coral-framework/services/i18n.js +++ b/client/coral-framework/services/i18n.js @@ -3,6 +3,8 @@ import has from 'lodash/has'; import get from 'lodash/get'; import merge from 'lodash/merge'; +import { createStorage } from 'coral-framework/services/storage'; + import moment from 'moment'; import 'moment/locale/da'; import 'moment/locale/es'; @@ -38,25 +40,34 @@ const translations = { let lang; let timeagoInstance; -function setLocale(locale) { +function setLocale(storage, locale) { try { - localStorage.setItem('locale', locale); + if (storage) { + storage.setItem('locale', locale); + } } catch (err) { console.error(err); } } -function getLocale() { - return ( - localStorage.getItem('locale') || - navigator.language || - defaultLanguage - ).split('-')[0]; +function getLocale(storage) { + try { + const locale = ( + (storage && storage.getItem('locale')) || + navigator.language || + defaultLanguage + ).split('-')[0]; + + return locale; + } catch (err) { + console.error(err); + return null; + } } -function init() { - const locale = getLocale(); - setLocale(locale); +export function setupTranslations(storage) { + const locale = getLocale(storage); + setLocale(storage, locale); // Setting moment moment.locale(locale); @@ -113,5 +124,3 @@ export function t(key, ...replacements) { } export default t; - -init(); From c612902779d8ac890726f443d5fbb74de5b53f0a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 17 Jan 2018 13:41:02 -0700 Subject: [PATCH 4/5] fixed styles --- client/coral-framework/services/bootstrap.js | 6 +----- client/coral-framework/services/i18n.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 912c6c532..b1014f2db 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -5,7 +5,7 @@ import EventEmitter from 'eventemitter2'; import { createReduxEmitter } from './events'; import { createRestClient } from './rest'; import thunk from 'redux-thunk'; -import { setupTranslations, loadTranslations } from './i18n'; +import { loadTranslations } from './i18n'; import bowser from 'bowser'; import noop from 'lodash/noop'; import { BASE_PATH } from 'coral-framework/constants/url'; @@ -88,10 +88,6 @@ export async function createContext({ const pymStorage = createPymStorage(pym); const history = createHistory(BASE_PATH); const introspection = createIntrospection(introspectionData); - - // Setup the translation framework with the storage. - setupTranslations(storage); - let store = null; const token = () => { // Try to get the token from localStorage. If it isn't here, it may diff --git a/client/coral-framework/services/i18n.js b/client/coral-framework/services/i18n.js index 4c387a0ca..91f8560cc 100644 --- a/client/coral-framework/services/i18n.js +++ b/client/coral-framework/services/i18n.js @@ -3,14 +3,14 @@ import has from 'lodash/has'; import get from 'lodash/get'; import merge from 'lodash/merge'; -import { createStorage } from 'coral-framework/services/storage'; - import moment from 'moment'; import 'moment/locale/da'; import 'moment/locale/es'; import 'moment/locale/fr'; import 'moment/locale/pt-br'; +import { createStorage } from 'coral-framework/services/storage'; + import daTA from 'timeago.js/locales/da'; import esTA from 'timeago.js/locales/es'; import frTA from 'timeago.js/locales/fr'; @@ -52,20 +52,21 @@ function setLocale(storage, locale) { function getLocale(storage) { try { - const locale = ( + return ( (storage && storage.getItem('locale')) || navigator.language || defaultLanguage ).split('-')[0]; - - return locale; } catch (err) { console.error(err); return null; } } -export function setupTranslations(storage) { +export function setupTranslations() { + // Setup the translation framework with the storage. + const storage = createStorage(); + const locale = getLocale(storage); setLocale(storage, locale); @@ -124,3 +125,6 @@ export function t(key, ...replacements) { } export default t; + +// Setup the translations globally as soon as this module runs. +setupTranslations(); From ac62cbf348783f314514ea66c34e41b5a0b0d977 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 17 Jan 2018 16:04:32 -0700 Subject: [PATCH 5/5] added new closeAsset mutation and support in UI --- .../configure/containers/AssetStatusInfo.js | 17 +++++--- client/coral-framework/graphql/fragments.js | 31 ++++++------- client/coral-framework/graphql/mutations.js | 43 +++++++++++++++++++ graph/mutators/asset.js | 20 +++++++++ graph/resolvers/root_mutation.js | 3 ++ graph/typeDefs.graphql | 11 +++++ 6 files changed, 104 insertions(+), 21 deletions(-) diff --git a/client/coral-embed-stream/src/tabs/configure/containers/AssetStatusInfo.js b/client/coral-embed-stream/src/tabs/configure/containers/AssetStatusInfo.js index 8fb6dd2ef..ed0746d29 100644 --- a/client/coral-embed-stream/src/tabs/configure/containers/AssetStatusInfo.js +++ b/client/coral-embed-stream/src/tabs/configure/containers/AssetStatusInfo.js @@ -3,15 +3,15 @@ import { gql, compose } from 'react-apollo'; import { withFragments } from 'coral-framework/hocs'; import AssetStatusInfo from '../components/AssetStatusInfo'; import PropTypes from 'prop-types'; -import { withUpdateAssetStatus } from 'coral-framework/graphql/mutations'; +import { + withUpdateAssetStatus, + withCloseAsset, +} from 'coral-framework/graphql/mutations'; class AssetStatusInfoContainer extends React.Component { openAsset = () => this.props.updateAssetStatus(this.props.asset.id, { closedAt: null }); - closeAsset = () => - this.props.updateAssetStatus(this.props.asset.id, { - closedAt: new Date().toISOString(), - }); + closeAsset = () => this.props.closeAsset(this.props.asset.id); render() { return ( @@ -29,6 +29,7 @@ class AssetStatusInfoContainer extends React.Component { AssetStatusInfoContainer.propTypes = { asset: PropTypes.object.isRequired, updateAssetStatus: PropTypes.func.isRequired, + closeAsset: PropTypes.func.isRequired, }; const withAssetStatusInfoFragments = withFragments({ @@ -41,6 +42,10 @@ const withAssetStatusInfoFragments = withFragments({ `, }); -const enhance = compose(withAssetStatusInfoFragments, withUpdateAssetStatus); +const enhance = compose( + withAssetStatusInfoFragments, + withUpdateAssetStatus, + withCloseAsset +); export default enhance(AssetStatusInfoContainer); diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js index 2ec92ea6b..a62fd6d92 100644 --- a/client/coral-framework/graphql/fragments.js +++ b/client/coral-framework/graphql/fragments.js @@ -3,27 +3,28 @@ import { createDefaultResponseFragments } from '../utils'; // fragments defined here are automatically registered. export default { ...createDefaultResponseFragments( - 'SetUserRoleResponse', - 'ChangeUsernameResponse', - 'SetUsernameResponse', 'BanUsersResponse', - 'UnbanUserResponse', - 'SetUserSuspensionStatusResponse', - 'SetCommentStatusResponse', - 'SetUsernameStatusResponse', - 'UnsuspendUserResponse', - 'SuspendUserResponse', + 'ChangeUsernameResponse', + 'CloseAssetResponse', 'CreateCommentResponse', - 'CreateFlagResponse', - 'EditCommentResponse', - 'PostFlagResponse', 'CreateDontAgreeResponse', + 'CreateFlagResponse', 'DeleteActionResponse', - 'ModifyTagResponse', + 'EditCommentResponse', 'IgnoreUserResponse', + 'ModifyTagResponse', + 'PostFlagResponse', + 'SetCommentStatusResponse', + 'SetUsernameResponse', + 'SetUsernameStatusResponse', + 'SetUserRoleResponse', + 'SetUserSuspensionStatusResponse', 'StopIgnoringUserResponse', - 'UpdateSettingsResponse', + 'SuspendUserResponse', + 'UnbanUserResponse', + 'UnsuspendUserResponse', 'UpdateAssetSettingsResponse', - 'UpdateAssetStatusResponse' + 'UpdateAssetStatusResponse', + 'UpdateSettingsResponse' ), }; diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index e5f0bb60e..f34a2cc88 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -665,3 +665,46 @@ export const withUpdateAssetStatus = withMutation( }), } ); + +export const withCloseAsset = withMutation( + gql` + mutation CloseAsset($id: ID!) { + closeAsset(id: $id) { + ...CloseAssetResponse + } + } + `, + { + props: ({ mutate }) => ({ + closeAsset: id => { + return mutate({ + variables: { + id, + }, + optimisticResponse: { + closeAsset: { + __typename: 'CloseAssetResponse', + errors: null, + }, + }, + update: proxy => { + const fragment = gql` + fragment Talk_CloseAssetResponse on Asset { + closedAt + isClosed + } + `; + + const fragmentId = `Asset_${id}`; + const data = { + __typename: 'Asset', + closedAt: new Date(), + isClosed: true, + }; + proxy.writeFragment({ fragment, id: fragmentId, data }); + }, + }); + }, + }), + } +); diff --git a/graph/mutators/asset.js b/graph/mutators/asset.js index 171c711f0..6524331e1 100644 --- a/graph/mutators/asset.js +++ b/graph/mutators/asset.js @@ -38,11 +38,30 @@ const updateStatus = async (ctx, id, { closedAt, closedMessage }) => } ); +/** + * closeNow will close an asset for commenting. + * + * @param {Object} ctx graphql context + * @param {String} id the asset's id to close + */ +const closeNow = async (ctx, id) => + AssetModel.update( + { + id, + }, + { + $set: { + closedAt: new Date(), + }, + } + ); + module.exports = ctx => { let mutators = { Asset: { updateSettings: () => Promise.reject(errors.ErrNotAuthorized), updateStatus: () => Promise.reject(errors.ErrNotAuthorized), + closeNow: () => Promise.reject(errors.ErrNotAuthorized), }, }; @@ -55,6 +74,7 @@ module.exports = ctx => { if (ctx.user.can(UPDATE_ASSET_STATUS)) { mutators.Asset.updateStatus = (id, status) => updateStatus(ctx, id, status); + mutators.Asset.closeNow = id => closeNow(ctx, id); } } diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index e99a8bd4b..3a646b3d8 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -94,6 +94,9 @@ const RootMutation = { ) => { await Asset.updateStatus(id, status); }, + closeAsset: async (_, { id }, { mutators: { Asset } }) => { + await Asset.closeNow(id); + }, setUserRole: async (_, { id, role }, { mutators: { User } }) => { await User.setRole(id, role); }, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 002318bc3..cd9eb9325 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1111,6 +1111,14 @@ type UpdateAssetStatusResponse implements Response { errors: [UserError!] } +# CloseAssetResponse is the response returned with possibly some errors +# relating to the update status attempt. +type CloseAssetResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + # UpdateAssetSettingsResponse is the response returned with possibly some errors # relating to the update settings attempt. type UpdateAssetSettingsResponse implements Response { @@ -1446,6 +1454,9 @@ type RootMutation { # Mutation is restricted. updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse + # closeAsset will close the asset for commenting based on server time. + closeAsset(id: ID!): CloseAssetResponse + # updateSettings will update the global settings. # Mutation is restricted. updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse