From 1137fca3b4d5b6f059ae10cb757eed5f61da0e25 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 18 Apr 2017 14:03:26 -0700 Subject: [PATCH 001/264] Adds Tag Model that will be apply to different models. --- graph/resolvers/tag.js | 0 models/comment.js | 22 ------ models/tag.js | 64 +++++++++++++++++ models/user.js | 6 +- services/comments.js | 67 ++++-------------- services/mongoose.js | 1 + services/tags.js | 72 ++++++++++++++++++++ test/server/graph/mutations/addCommentTag.js | 8 ++- 8 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 graph/resolvers/tag.js create mode 100644 models/tag.js create mode 100644 services/tags.js diff --git a/graph/resolvers/tag.js b/graph/resolvers/tag.js new file mode 100644 index 000000000..e69de29bb diff --git a/models/comment.js b/models/comment.js index 0984a0832..09f592eca 100644 --- a/models/comment.js +++ b/models/comment.js @@ -30,27 +30,6 @@ const StatusSchema = new Schema({ _id: false }); -/** - * The Mongo schema for a Comment Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - name: String, - - // The User ID of the user that assigned the status. - assigned_by: { - type: String, - default: null - }, - - created_at: { - type: Date, - default: Date - } -}, { - _id: false -}); - /** * The Mongo schema for a Comment. * @type {Schema} @@ -74,7 +53,6 @@ const CommentSchema = new Schema({ enum: STATUSES, default: 'NONE' }, - tags: [TagSchema], parent_id: String, // Additional metadata stored on the field. diff --git a/models/tag.js b/models/tag.js new file mode 100644 index 000000000..6991f3abb --- /dev/null +++ b/models/tag.js @@ -0,0 +1,64 @@ +const mongoose = require('../services/mongoose'); +const uuid = require('uuid'); +const Schema = mongoose.Schema; + +// in settings +// --> decide who can apply them (self, role, anyone) and + +// Who can see the tag (self, by role, anyone) +const PRIVACY_TYPES = [ + 'ADMIN', + 'SELF', + 'PUBLIC' +]; + +// The type of item that the tag is apply on. +const ITEM_TYPES = [ + 'ASSETS', + 'COMMENTS', + 'USERS' +]; + +/** + * The Mongo schema for a Comment Tag. + * @type {Schema} + */ +const TagSchema = new Schema({ + id: { + type: String, + default: uuid.v4, + unique: true + }, + + name: { + type: String, + unique: true + }, + + item_type: { + type: String, + enum: ITEM_TYPES + }, + + item_id: String, + + // The User ID of the user that assigned the status. + assigned_by: { + type: String, + default: null + }, + + privacy_type: { + type: String, + enum: PRIVACY_TYPES + }, + + // Additional metadata stored on the field. + metadata: Schema.Types.Mixed +}, { + _id: false +}); + +const Tag = mongoose.model('Tag', TagSchema); + +module.exports = Tag; diff --git a/models/user.js b/models/user.js index 2663410a7..fdd3ed74a 100644 --- a/models/user.js +++ b/models/user.js @@ -185,7 +185,9 @@ const USER_GRAPH_OPERATIONS = [ 'mutation:suspendUser', 'mutation:setCommentStatus', 'mutation:addCommentTag', - 'mutation:removeCommentTag' + 'mutation:removeCommentTag', + 'mutation:addUserTag', + 'mutation:removeUserTag' ]; /** @@ -207,7 +209,7 @@ UserSchema.method('can', function(...actions) { // {add,remove}CommentTag - requires admin and/or moderator role const userCanModifyTags = user => ['ADMIN', 'MODERATOR'].some(r => user.hasRoles(r)); - if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! userCanModifyTags(this)) { + if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag', 'mutation:removeUserTag', 'mutation: addUserTag'].includes(a)) && ! userCanModifyTags(this)) { return false; } diff --git a/services/comments.js b/services/comments.js index 75b53873a..a7f483d2c 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,10 +3,8 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const ALLOWED_TAGS = [ - {name: 'STAFF'}, - {name: 'BEST'}, -]; +const TagModel = require('../models/tag'); +const TagsService = require('./tags'); const STATUSES = [ 'ACCEPTED', @@ -53,37 +51,14 @@ module.exports = class CommentsService { */ static addTag(id, name, assigned_by) { - if (ALLOWED_TAGS.find((t) => t.name === name) == null) { - return Promise.reject(new Error('tag not allowed')); - } + return TagsService.insertCommentTag({ + name, + item_id: id, + item_type: 'COMMENTS', + user_id: assigned_by, + }); - const filter = { - id, - 'tags.name': {$ne: name}, - }; - const update = { - $push: {tags: { - name, - assigned_by, - created_at: new Date() - }} - }; - return CommentModel.update(filter, update) - .then(({nModified}) => { - switch (nModified) { - case 0: - - // either the tag was already there, or the comment doesn't exist with that id... - throw new Error('Could not add tag to comment. Either the comment doesn\'t exist or the tag is already present.'); - case 1: - - // tag added - return; - default: - - // this should never happen because no multi parameter and unique index on id - } - }); + // Add the ID to the comment } /** @@ -93,25 +68,11 @@ module.exports = class CommentsService { * @param {String} name the name of the tag to add */ static removeTag(id, name) { - const filter = { - id, - 'tags.name': name, - }; - const update = {$pull: {tags: {name}}}; - return CommentModel.update(filter, update) - .then(({nModified}) => { - switch (nModified) { - case 0: - throw new Error('Could not remove tag from comment. Either the comment doesn\'t exist or the tag is not present'); - case 1: - - // tag removed - return; - default: - - // this should never happen because no multi parameter and unique index on id - } - }); + return TagModel.remove({ + item_type: 'COMMENTS', + item_id: id, + name + }); } /** diff --git a/services/mongoose.js b/services/mongoose.js index c7092326f..206075239 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -65,4 +65,5 @@ require('../models/action'); require('../models/asset'); require('../models/comment'); require('../models/setting'); +require('../models/tag'); require('../models/user'); diff --git a/services/tags.js b/services/tags.js new file mode 100644 index 000000000..caeb4e761 --- /dev/null +++ b/services/tags.js @@ -0,0 +1,72 @@ +const TagModel = require('../models/tag'); + +const ALLOWED_COMMENT_TAGS = [ + {name: 'STAFF'}, + {name: 'BEST'}, +]; + +module.exports = class TagsService { + + /** + * Finds an action by the id. + * @param {String} id identifier of the tag (uuid) + */ + static findById(id) { + return TagModel.findOne({id}); + } + + /** + * Add a tag. + * @param {string} name the actual tag + * @param {String} item_id identifier of the comment (uuid) + * @param {String} item_type type of the object being tag (COMMENTS) + * @param {String} user_id user id that assigned the tag (uuid) + * @param {String} privacy_type visibility of the tag on the comment + * @return {Promise} + */ + static insertCommentTag(tag) { + + if (ALLOWED_COMMENT_TAGS.find((t) => t.name === tag.name) == null) { + return Promise.reject(new Error('tag not allowed')); + } + + // Tags are made unique by using a query that can be reproducable, i.e., + // not containing user inputable values. + let query = { + name: tag.name, + item_id: tag.item_id, + item_type: tag.item_type, + assigned_by: tag.user_id, + privacy_type: tag.privacy_type + }; + + // Create/Update the tag. + return TagModel.findOneAndUpdate(query, tag, { + + // Ensure that if it's new, we return the new object created. + new: true, + + // Perform an upsert in the event that this doesn't exist. + upsert: true, + + // Set the default values if not provided based on the mongoose models. + setDefaultsOnInsert: true + }) + .then(({nModified}) => { + switch (nModified) { + case 0: + + // either the tag was already there, or the comment doesn't exist with that id... + throw new Error('Could not add tag to comment. Either the comment doesn\'t exist or the tag is already present.'); + case 1: + + // tag added + return; + default: + + // this should never happen because no multi parameter and unique index on id + } + }); + } + +}; diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 018e96631..fbb7fe8a8 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -4,6 +4,7 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); +const TagModel = require('../../../../models/tag'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -37,7 +38,12 @@ describe('graph.mutations.addCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; - expect(response.data.addCommentTag.comment.tags).to.deep.equal([{name: 'BEST'}]); + TagModel.find({ + item_id: response.data.addCommentTag.comment.id, + name: 'BEST' + }).then((tags) => { + expect(tags).to.have.length(1); + }); }); describe('users who cant add tags', () => { From bd72bfc6ecd5a5f5b888aff7b97f8f069fada01e Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 20 Apr 2017 12:24:08 -0700 Subject: [PATCH 002/264] Fix tests related to tags on comments. --- graph/loaders/index.js | 2 + graph/loaders/tags.js | 37 ++++++++++++ graph/mutators/comment.js | 14 ++--- models/tag.js | 10 +++- services/comments.js | 27 ++++++--- services/tags.js | 59 +++++++++---------- test/server/graph/mutations/addCommentTag.js | 13 ++-- test/server/graph/mutations/createComment.js | 14 +++-- .../graph/mutations/removeCommentTag.js | 16 +++-- test/server/services/comments.js | 28 +++++---- 10 files changed, 139 insertions(+), 81 deletions(-) create mode 100644 graph/loaders/tags.js diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 26727940f..8bc170c6d 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -6,6 +6,7 @@ const Assets = require('./assets'); const Comments = require('./comments'); const Metrics = require('./metrics'); const Settings = require('./settings'); +const Tags = require('./tags'); const Users = require('./users'); const plugins = require('../../services/plugins'); @@ -18,6 +19,7 @@ let loaders = [ Comments, Metrics, Settings, + Tags, Users, // Load the plugin loaders from the manager. diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js new file mode 100644 index 000000000..b76bc36bd --- /dev/null +++ b/graph/loaders/tags.js @@ -0,0 +1,37 @@ +const DataLoader = require('dataloader'); + +const util = require('./util'); + +const TagsService = require('../../services/tags'); +const TagModel = require('../../models/tag'); + +/** + * Gets tags based on their item id's. + */ +const genTagsByItemID = (_, item_ids) => { + return TagsService + .findByItemIdArray(item_ids) + .then(util.arrayJoinBy(item_ids, 'item_id')); +}; + +/** + * Search for tags based on their item_type and ensures that + * the tags returned have unique item id's. + * @param {String} item_type the item id to search by + * @return {Promise} resolves to distinct items tags + */ +const getItemIdsByItemType = (_, item_type) => { + return TagModel.distinct('item_id', {item_type}); +}; + +/** + * Creates a set of loaders based on a GraphQL context. + * @param {Object} context the context of the GraphQL request + * @return {Object} object of loaders + */ +module.exports = (context) => ({ + Tags: { + getByID: new DataLoader((ids) => genTagsByItemID(context, ids)), + getByTypes: ({item_type}) => getItemIdsByItemType(context, item_type) + } +}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 728603ae2..930417c5c 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -5,6 +5,8 @@ const ActionsService = require('../../services/actions'); const CommentsService = require('../../services/comments'); const linkify = require('linkify-it')(); +const TagModel = require('../../models/tag'); + const Wordlist = require('../../services/wordlist'); /** @@ -18,20 +20,18 @@ const Wordlist = require('../../services/wordlist'); */ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = 'NONE') => { - let tags = []; - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - tags = [{name: 'STAFF'}]; - } - return CommentsService.publicCreate({ body, asset_id, parent_id, status, - tags, author_id: user.id }) - .then((comment) => { + .then(async (comment) => { + + if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + await CommentsService.addTag(comment.id, 'STAFF', user.id); + } // If the loaders are present, clear the caches for these values because we // just added a new comment, hence the counts should be updated. We should diff --git a/models/tag.js b/models/tag.js index 6991f3abb..155e57dbb 100644 --- a/models/tag.js +++ b/models/tag.js @@ -50,13 +50,17 @@ const TagSchema = new Schema({ privacy_type: { type: String, - enum: PRIVACY_TYPES + enum: PRIVACY_TYPES, + default: 'SELF' }, // Additional metadata stored on the field. metadata: Schema.Types.Mixed -}, { - _id: false +}, { + timestamps: { + createdAt: 'created_at', + updatedAt: 'updated_at' + } }); const Tag = mongoose.model('Tag', TagSchema); diff --git a/services/comments.js b/services/comments.js index a7f483d2c..d61b6ca25 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,8 +3,8 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const TagModel = require('../models/tag'); const TagsService = require('./tags'); +const TagModel = require('../models/tag'); const STATUSES = [ 'ACCEPTED', @@ -51,14 +51,18 @@ module.exports = class CommentsService { */ static addTag(id, name, assigned_by) { - return TagsService.insertCommentTag({ - name, - item_id: id, - item_type: 'COMMENTS', - user_id: assigned_by, + return CommentModel.findOne({id}) + .then((comment) => { + if (comment == null) { + return Promise.reject(new Error('tag not allowed')); + } + return TagsService.insertCommentTag({ + name, + item_id: id, + item_type: 'COMMENTS', + user_id: assigned_by, + }); }); - - // Add the ID to the comment } /** @@ -68,10 +72,15 @@ module.exports = class CommentsService { * @param {String} name the name of the tag to add */ static removeTag(id, name) { - return TagModel.remove({ + return TagModel.findOneAndRemove({ item_type: 'COMMENTS', item_id: id, name + }) + .then((tag) => { + if (tag == null) { + return Promise.reject(new Error('tag does not exist')); + } }); } diff --git a/services/tags.js b/services/tags.js index caeb4e761..172148727 100644 --- a/services/tags.js +++ b/services/tags.js @@ -8,13 +8,26 @@ const ALLOWED_COMMENT_TAGS = [ module.exports = class TagsService { /** - * Finds an action by the id. + * Finds a tag by the id. * @param {String} id identifier of the tag (uuid) */ static findById(id) { return TagModel.findOne({id}); } + /** + * Finds atag by the item_id and name. + * @param {String} item_id identifier of the item that the tag was applied into(uuid) + * @param {string} name name of the tag + */ + static findByItemIdAndName(item_id, name, item_type) { + return TagModel.find({ + item_id, + item_type, + name + }); + } + /** * Add a tag. * @param {string} name the actual tag @@ -30,43 +43,25 @@ module.exports = class TagsService { return Promise.reject(new Error('tag not allowed')); } - // Tags are made unique by using a query that can be reproducable, i.e., - // not containing user inputable values. - let query = { + // // Tags are made unique by using a query that can be reproducable, i.e., + // // not containing user inputable values. + // let query = { + // name: tag.name, + // item_id: tag.item_id, + // item_type: tag.item_type, + // assigned_by: tag.user_id, + // privacy_type: tag.privacy_type + // }; + + // Create/Update the tag. + let newtag = new TagModel({ name: tag.name, item_id: tag.item_id, item_type: tag.item_type, assigned_by: tag.user_id, privacy_type: tag.privacy_type - }; - - // Create/Update the tag. - return TagModel.findOneAndUpdate(query, tag, { - - // Ensure that if it's new, we return the new object created. - new: true, - - // Perform an upsert in the event that this doesn't exist. - upsert: true, - - // Set the default values if not provided based on the mongoose models. - setDefaultsOnInsert: true - }) - .then(({nModified}) => { - switch (nModified) { - case 0: - - // either the tag was already there, or the comment doesn't exist with that id... - throw new Error('Could not add tag to comment. Either the comment doesn\'t exist or the tag is already present.'); - case 1: - - // tag added - return; - default: - - // this should never happen because no multi parameter and unique index on id - } }); + return newtag.save(); } }; diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index fbb7fe8a8..063537004 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -4,7 +4,7 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); -const TagModel = require('../../../../models/tag'); +const TagService = require('../../../../services/tags'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -19,9 +19,7 @@ describe('graph.mutations.addCommentTag', () => { mutation AddCommentTag ($id: ID!, $tag: String!) { addCommentTag(id:$id, tag:$tag) { comment { - tags { - name - } + id } errors { translation_key @@ -38,10 +36,9 @@ describe('graph.mutations.addCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; - TagModel.find({ - item_id: response.data.addCommentTag.comment.id, - name: 'BEST' - }).then((tags) => { + + return TagService.findByItemIdAndName(response.data.addCommentTag.comment.id, 'BEST', 'COMMENTS') + .then((tags) => { expect(tags).to.have.length(1); }); }); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index b3bdd459a..11fa666d0 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -3,11 +3,14 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); + const UserModel = require('../../../../models/user'); const AssetModel = require('../../../../models/asset'); -const SettingsService = require('../../../../services/settings'); const ActionModel = require('../../../../models/action'); +const SettingsService = require('../../../../services/settings'); +const TagService = require('../../../../services/tags'); + describe('graph.mutations.createComment', () => { beforeEach(() => SettingsService.init()); @@ -217,11 +220,14 @@ describe('graph.mutations.createComment', () => { expect(data.createComment).to.have.property('comment').not.null; expect(data.createComment).to.have.property('errors').null; + return TagService.findByItemIdAndName(data.createComment.comment.id, tag, 'COMMENTS'); + }) + .then((tags) => { if (tag) { - expect(data.createComment.comment).to.have.property('tags').length(1); - expect(data.createComment.comment.tags[0]).to.have.property('name', tag); + expect(tags).to.have.length(1); + expect(tags[0]).to.have.property('name', tag); } else { - expect(data.createComment.comment).to.have.property('tags').length(0); + expect(tags).length(0); } }); }); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index c0d452f37..4455a0108 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -4,8 +4,10 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); + const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); +const TagService = require('../../../../services/tags'); describe('graph.mutations.removeCommentTag', () => { let comment; @@ -18,9 +20,7 @@ describe('graph.mutations.removeCommentTag', () => { mutation RemoveCommentTag ($id: ID!, $tag: String!) { removeCommentTag(id:$id, tag:$tag) { comment { - tags { - name - } + id } errors { translation_key @@ -41,7 +41,12 @@ describe('graph.mutations.removeCommentTag', () => { } expect(response.errors).to.be.empty; expect(response.data.removeCommentTag.errors).to.be.null; - expect(response.data.removeCommentTag.comment.tags).to.deep.equal([]); + + TagService.findByItemIdAndName(response.data.removeCommentTag.comment.id, 'BEST') + .then((tags) => { + expect(tags).to.deep.equal([]); + }); + }); describe('users who cant remove tags', () => { @@ -60,8 +65,9 @@ describe('graph.mutations.removeCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; + expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); - expect(response.data.removeCommentTag.comment).to.be.null; + expect(response.data.removeCommentTag.comment).to.be.null; }); }); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 5b9cbf4d1..952e071a3 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -5,6 +5,7 @@ const ActionsService = require('../../../services/actions'); const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const CommentsService = require('../../../services/comments'); +const TagService = require('../../../services/tags'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; @@ -225,16 +226,17 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; await CommentsService.addTag(commentId, tagName, userId); - const updatedComment = await CommentsService.findById(commentId); - expect(updatedComment.tags.length).to.equal(1); - expect(updatedComment.tags[0].name).to.equal(tagName); - expect(updatedComment.tags[0].assigned_by).to.equal(userId); - expect(updatedComment.tags[0].created_at).to.be.an.instanceof(Date); + const tags = await TagService.findByItemIdAndName(commentId, 'BEST', 'COMMENTS'); + expect(tags.length).to.equal(1); + expect(tags[0].name).to.equal(tagName); + expect(tags[0].assigned_by).to.equal(userId); + expect(tags[0].created_at).to.be.an.instanceof(Date); }); it('can\'t add a tag to comment id that doesn\'t exist', async () => { const commentId = 'fakenews'; const tagName = 'BEST'; const userId = users[0].id; + await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; }); it('can\'t add same tag.name twice', async () => { @@ -255,23 +257,23 @@ describe('services.CommentsService', () => { const commentId = comments[0].id; const tagName = 'BEST'; await CommentsService.addTag(commentId, tagName, users[0].id); - const updatedComment = await CommentsService.findById(commentId); - expect(updatedComment.tags.length).to.equal(1); + const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + expect(tags.length).to.equal(1); // ok now to remove it await CommentsService.removeTag(commentId, tagName); - const updatedComment2 = await CommentsService.findById(commentId); - expect(updatedComment2.tags.length).to.equal(0); + const tags2 = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + expect(tags2.length).to.equal(0); }); it('throws if removing a tag that isn\'t there', async () => { const commentId = comments[0].id; - // just make sure it has no tags to start - const updatedComment2 = await CommentsService.findById(commentId); - expect(updatedComment2.tags.length).to.equal(0); - const tagName = 'BEST'; + // just make sure it has no tags to start + const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + expect(tags.length).to.equal(0); + // ok now to remove it await expect(CommentsService.removeTag(commentId, tagName)).to.be.rejected; }); From 8458a9aecc160cca60e88d7809026395a2d9bb91 Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 20 Apr 2017 12:29:21 -0700 Subject: [PATCH 003/264] fixed lint error. --- graph/mutators/comment.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 930417c5c..096db4913 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -5,8 +5,6 @@ const ActionsService = require('../../services/actions'); const CommentsService = require('../../services/comments'); const linkify = require('linkify-it')(); -const TagModel = require('../../models/tag'); - const Wordlist = require('../../services/wordlist'); /** From 962a9bd6c91fed0e6674d4e7076a86e21a90bf21 Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 20 Apr 2017 14:42:57 -0700 Subject: [PATCH 004/264] That was so so so silly... --- models/tag.js | 1 - 1 file changed, 1 deletion(-) diff --git a/models/tag.js b/models/tag.js index 155e57dbb..b6e29a6ab 100644 --- a/models/tag.js +++ b/models/tag.js @@ -32,7 +32,6 @@ const TagSchema = new Schema({ name: { type: String, - unique: true }, item_type: { From 91af8c2ecec35c275d3867876f058aab6ab1832b Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 21 Apr 2017 10:34:57 -0700 Subject: [PATCH 005/264] Index on Tag for name, item id, item type. --- graph/loaders/tags.js | 4 ++-- graph/resolvers/comment.js | 7 +++++-- models/tag.js | 10 ++++++++++ services/tags.js | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js index b76bc36bd..cb767fdf6 100644 --- a/graph/loaders/tags.js +++ b/graph/loaders/tags.js @@ -8,7 +8,7 @@ const TagModel = require('../../models/tag'); /** * Gets tags based on their item id's. */ -const genTagsByItemID = (_, item_ids) => { +const getByItemID = (_, item_ids) => { return TagsService .findByItemIdArray(item_ids) .then(util.arrayJoinBy(item_ids, 'item_id')); @@ -31,7 +31,7 @@ const getItemIdsByItemType = (_, item_type) => { */ module.exports = (context) => ({ Tags: { - getByID: new DataLoader((ids) => genTagsByItemID(context, ids)), + getByID: new DataLoader((ids) => getByItemID(context, ids)), getByTypes: ({item_type}) => getItemIdsByItemType(context, item_type) } }); diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 19ea11efe..f94cfaf52 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -6,6 +6,9 @@ const Comment = { return Comments.get.load(parent_id); }, + tags({id}, _, {loaders: {Tags}}) { + return Tags.getByID.load([id]); + }, user({author_id}, _, {loaders: {Users}}) { return Users.getByID.load(author_id); }, @@ -23,9 +26,9 @@ const Comment = { }, replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); + return Comments.countByParentIDPersonalized({id, excludeIgnored}); } - return Comments.countByParentID.load(id); + return Comments.countByParentID.load(id); }, actions({id}, _, {user, loaders: {Actions}}) { diff --git a/models/tag.js b/models/tag.js index b6e29a6ab..85bb0845a 100644 --- a/models/tag.js +++ b/models/tag.js @@ -62,6 +62,16 @@ const TagSchema = new Schema({ } }); +// Add the indixies on the tag on an item. +TagSchema.index({ + 'item_type': 1, + 'item_id': 1, + 'name': 1 +}, { + unique: true, + background: false +}); + const Tag = mongoose.model('Tag', TagSchema); module.exports = Tag; diff --git a/services/tags.js b/services/tags.js index 172148727..c4010e562 100644 --- a/services/tags.js +++ b/services/tags.js @@ -28,6 +28,22 @@ module.exports = class TagsService { }); } + /** + * Finds actions in an array of ids. + * @param {String} ids array of user identifiers (uuid) + */ + static async findByItemIdArray(item_ids) { + let tags = await TagModel.find({ + 'item_id': {$in: item_ids} + }); + + if (tags === null) { + return []; + } + + return tags; + } + /** * Add a tag. * @param {string} name the actual tag From c4ffe58f585e66a5a863d6020dc633677919d97f Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 21 Apr 2017 14:30:41 -0700 Subject: [PATCH 006/264] Adds privacy type to the methods to add tags. --- graph/mutators/comment.js | 6 +++--- graph/resolvers/root_mutation.js | 4 ++-- graph/typeDefs.graphql | 2 +- services/comments.js | 3 ++- services/tags.js | 18 +++++++++--------- test/server/graph/mutations/addCommentTag.js | 8 ++++---- .../server/graph/mutations/removeCommentTag.js | 4 ++-- test/server/services/comments.js | 12 ++++++------ 8 files changed, 29 insertions(+), 28 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 0290d9f16..724e71917 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id); + await CommentsService.addTag(comment.id, 'STAFF', user.id, 'PUBLIC'); } // If the loaders are present, clear the caches for these values because we @@ -202,8 +202,8 @@ const setCommentStatus = ({loaders: {Comments}}, {id, status}) => { * @param {String} id identifier of the comment (uuid) * @param {String} tag name of the tag */ -const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user.id); +const addCommentTag = ({user, loaders: {Comments}}, {id, tag, privacy_type}) => { + return CommentsService.addTag(id, tag, user.id, privacy_type); }; /** diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 661cc9f96..d669869cd 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -32,8 +32,8 @@ const RootMutation = { setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, - addCommentTag(_, {id, tag}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.addCommentTag({id, tag}).then(() => CommentsService.findById(id))); + addCommentTag(_, {id, tag, privacy_type}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.addCommentTag({id, tag, privacy_type}).then(() => CommentsService.findById(id))); }, removeCommentTag(_, {id, tag}, {mutators: {Comment}}) { return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id))); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 32c2c16c2..200e752b6 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -808,7 +808,7 @@ type RootMutation { setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse # Add tag to comment. - addCommentTag(id: ID!, tag: String!): AddCommentTagResponse + addCommentTag(id: ID!, tag: String!, privacy_type: String!): AddCommentTagResponse # Remove tag from comment. removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse diff --git a/services/comments.js b/services/comments.js index d61b6ca25..ccb5a9131 100644 --- a/services/comments.js +++ b/services/comments.js @@ -49,7 +49,7 @@ module.exports = class CommentsService { * @param {String} name the name of the tag to add * @param {String} assigned_by the user id for the user who added the tag */ - static addTag(id, name, assigned_by) { + static addTag(id, name, assigned_by, privacy_type) { return CommentModel.findOne({id}) .then((comment) => { @@ -61,6 +61,7 @@ module.exports = class CommentsService { item_id: id, item_type: 'COMMENTS', user_id: assigned_by, + privacy_type }); }); } diff --git a/services/tags.js b/services/tags.js index c4010e562..5cd0a09e1 100644 --- a/services/tags.js +++ b/services/tags.js @@ -5,6 +5,12 @@ const ALLOWED_COMMENT_TAGS = [ {name: 'BEST'}, ]; +const ALLOWED_PRIVACY_TYPE = [ + {privacy_type: 'PUBLIC'}, + {privacy_type: 'SELF'}, + {privacy_type: 'ADMIN'} +]; + module.exports = class TagsService { /** @@ -59,15 +65,9 @@ module.exports = class TagsService { return Promise.reject(new Error('tag not allowed')); } - // // Tags are made unique by using a query that can be reproducable, i.e., - // // not containing user inputable values. - // let query = { - // name: tag.name, - // item_id: tag.item_id, - // item_type: tag.item_type, - // assigned_by: tag.user_id, - // privacy_type: tag.privacy_type - // }; + if (ALLOWED_PRIVACY_TYPE.find((p) => p.privacy_type === tag.privacy_type) == null) { + return Promise.reject(new Error('privacy type not allowed')); + } // Create/Update the tag. let newtag = new TagModel({ diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 063537004..01966bd88 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -16,8 +16,8 @@ describe('graph.mutations.addCommentTag', () => { }); const query = ` - mutation AddCommentTag ($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { + mutation AddCommentTag ($id: ID!, $tag: String!, $privacy_type: String!) { + addCommentTag(id:$id, tag:$tag, privacy_type:$privacy_type) { comment { id } @@ -31,7 +31,7 @@ describe('graph.mutations.addCommentTag', () => { it('moderators can add tags to comments', async () => { const user = new UserModel({roles: ['MODERATOR' ]}); const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); + const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST', privacy_type: 'PUBLIC'}); if (response.errors && response.errors.length) { console.error(response.errors); } @@ -51,7 +51,7 @@ describe('graph.mutations.addCommentTag', () => { }).forEach(([ userDescription, user ]) => { it(userDescription, async function () { const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); + const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST', privacy_type: 'PUBLIC'}); if (response.errors && response.errors.length) { console.error(response.errors); } diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 4455a0108..7660eef35 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -34,7 +34,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST'); + await CommentsService.addTag(comment.id, 'BEST', user.id, 'PUBLIC'); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); @@ -59,7 +59,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST'); + await CommentsService.addTag(comment.id, 'BEST', user ? user.id : null, 'PUBLIC'); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 952e071a3..d4fc5f17e 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -225,7 +225,7 @@ describe('services.CommentsService', () => { const commentId = comments[0].id; const tagName = 'BEST'; const userId = users[0].id; - await CommentsService.addTag(commentId, tagName, userId); + await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); const tags = await TagService.findByItemIdAndName(commentId, 'BEST', 'COMMENTS'); expect(tags.length).to.equal(1); expect(tags[0].name).to.equal(tagName); @@ -237,7 +237,7 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; - await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; + await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; }); it('can\'t add same tag.name twice', async () => { const commentId = comments[0].id; @@ -245,10 +245,10 @@ describe('services.CommentsService', () => { const userId = users[0].id; // first time - await CommentsService.addTag(commentId, tagName, userId); + await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); // second time should fail - await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; + await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; }); }); @@ -256,7 +256,7 @@ describe('services.CommentsService', () => { it('removes a tag', async () => { const commentId = comments[0].id; const tagName = 'BEST'; - await CommentsService.addTag(commentId, tagName, users[0].id); + await CommentsService.addTag(commentId, tagName, users[0].id, 'PUBLIC'); const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); expect(tags.length).to.equal(1); @@ -273,7 +273,7 @@ describe('services.CommentsService', () => { // just make sure it has no tags to start const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); expect(tags.length).to.equal(0); - + // ok now to remove it await expect(CommentsService.removeTag(commentId, tagName)).to.be.rejected; }); From cb09c5082010f32e924beab7375544f7a11a8c42 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 13:11:48 -0700 Subject: [PATCH 007/264] Error on duplicate. --- graph/loaders/index.js | 2 - graph/loaders/settings.js | 17 +++- graph/loaders/tags.js | 37 --------- graph/mutators/comment.js | 8 +- graph/resolvers/comment.js | 3 - graph/resolvers/root_mutation.js | 4 +- graph/typeDefs.graphql | 17 ++-- models/comment.js | 25 ++++++ models/setting.js | 38 ++++++++- models/tag.js | 77 ----------------- models/user.js | 12 +++ services/comments.js | 54 ++++++------ services/mongoose.js | 1 - services/tags.js | 83 ------------------- test/server/graph/loaders/metrics.js | 28 +++++-- test/server/graph/mutations/addCommentTag.js | 11 ++- test/server/graph/mutations/createComment.js | 6 +- .../graph/mutations/removeCommentTag.js | 5 +- test/server/services/comments.js | 11 ++- 19 files changed, 173 insertions(+), 266 deletions(-) delete mode 100644 graph/loaders/tags.js delete mode 100644 models/tag.js delete mode 100644 services/tags.js diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 8bc170c6d..26727940f 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -6,7 +6,6 @@ const Assets = require('./assets'); const Comments = require('./comments'); const Metrics = require('./metrics'); const Settings = require('./settings'); -const Tags = require('./tags'); const Users = require('./users'); const plugins = require('../../services/plugins'); @@ -19,7 +18,6 @@ let loaders = [ Comments, Metrics, Settings, - Tags, Users, // Load the plugin loaders from the manager. diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js index 77ece165f..9d777255f 100644 --- a/graph/loaders/settings.js +++ b/graph/loaders/settings.js @@ -1,12 +1,25 @@ const SettingsService = require('../../services/settings'); +const SettingModel = require('../../models/setting'); const util = require('./util'); +/** + * Search for tags based on their item_type. + * @param {String} item_type the item type to search by + * @return {Promise} resolves to distinct items tags + */ +const getByItemType = (_, item_type) => { + return SettingModel.distinct('tags.models', {item_type}); +}; + /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request * @return {Object} object of loaders */ -module.exports = () => ({ - Settings: new util.SingletonResolver(() => SettingsService.retrieve()) +module.exports = (context) => ({ + Settings: { + get: new util.SingletonResolver(() => SettingsService.retrieve()), + getTagsByItemType: (item_type) => getByItemType(context, item_type) + } }); diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js deleted file mode 100644 index cb767fdf6..000000000 --- a/graph/loaders/tags.js +++ /dev/null @@ -1,37 +0,0 @@ -const DataLoader = require('dataloader'); - -const util = require('./util'); - -const TagsService = require('../../services/tags'); -const TagModel = require('../../models/tag'); - -/** - * Gets tags based on their item id's. - */ -const getByItemID = (_, item_ids) => { - return TagsService - .findByItemIdArray(item_ids) - .then(util.arrayJoinBy(item_ids, 'item_id')); -}; - -/** - * Search for tags based on their item_type and ensures that - * the tags returned have unique item id's. - * @param {String} item_type the item id to search by - * @return {Promise} resolves to distinct items tags - */ -const getItemIdsByItemType = (_, item_type) => { - return TagModel.distinct('item_id', {item_type}); -}; - -/** - * Creates a set of loaders based on a GraphQL context. - * @param {Object} context the context of the GraphQL request - * @return {Object} object of loaders - */ -module.exports = (context) => ({ - Tags: { - getByID: new DataLoader((ids) => getByItemID(context, ids)), - getByTypes: ({item_type}) => getItemIdsByItemType(context, item_type) - } -}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index ec310d763..3413c6f4e 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -16,7 +16,7 @@ const Wordlist = require('../../services/wordlist'); * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => { +const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null}, status = 'NONE') => { return CommentsService.publicCreate({ body, @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id, 'PUBLIC'); + await CommentsService.addTag(comment.id, 'STAFF', user.id); } // If the loaders are present, clear the caches for these values because we @@ -208,8 +208,8 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { * @param {String} id identifier of the comment (uuid) * @param {String} tag name of the tag */ -const addCommentTag = ({user, loaders: {Comments}}, {id, tag, privacy_type}) => { - return CommentsService.addTag(id, tag, user.id, privacy_type); +const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { + return CommentsService.addTag(id, tag, user.id); }; /** diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index f94cfaf52..88ddbee07 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -6,9 +6,6 @@ const Comment = { return Comments.get.load(parent_id); }, - tags({id}, _, {loaders: {Tags}}) { - return Tags.getByID.load([id]); - }, user({author_id}, _, {loaders: {Users}}) { return Users.getByID.load(author_id); }, diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index e3e3ae1d7..5b60fbaa8 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -29,8 +29,8 @@ const RootMutation = { setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, - addCommentTag(_, {id, tag, privacy_type}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.addCommentTag({id, tag, privacy_type}).then(() => CommentsService.findById(id))); + addCommentTag(_, {id, tag}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.addCommentTag({id, tag}).then(() => CommentsService.findById(id))); }, removeCommentTag(_, {id, tag}, {mutators: {Comment}}) { return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id))); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index a4ebe98b3..781b3b328 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -54,10 +54,10 @@ type User { type Tag { # the actual tag for the comment. - name: String! + id: String! # the user that assigned the tag. If NULL then the system automatically tagged it. - assigned_by: String + added_by: String # the time when the tag was assigned. created_at: Date! @@ -588,8 +588,13 @@ enum ACTION_ITEM_TYPE { USERS } -enum TAG_TYPE { - STAFF +input CreateLikeInput { + + # The item's id for which we are to create a like. + item_id: ID! + + # The type of the item for which we are to create the like. + item_type: ACTION_ITEM_TYPE! } input CreateCommentInput { @@ -604,7 +609,7 @@ input CreateCommentInput { body: String! # Tags - tags: [TAG_TYPE] + tags: [String] } @@ -746,7 +751,7 @@ type RootMutation { setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse # Add tag to comment. - addCommentTag(id: ID!, tag: String!, privacy_type: String!): AddCommentTagResponse + addCommentTag(id: ID!, tag: String!): AddCommentTagResponse # Remove tag from comment. removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse diff --git a/models/comment.js b/models/comment.js index 09f592eca..f3e239080 100644 --- a/models/comment.js +++ b/models/comment.js @@ -30,6 +30,29 @@ const StatusSchema = new Schema({ _id: false }); +/** + * The Mongo schema for a Comment Tag. + * @type {Schema} + */ +const TagSchema = new Schema({ + + // This is the actual 'tag' and we only permit tags that are in Setting.tags. + id: String, + + // The User ID of the user that added the tag. + added_by: { + type: String, + default: null + }, + + created_at: { + type: Date, + default: Date + } +}, { + _id: false +}); + /** * The Mongo schema for a Comment. * @type {Schema} @@ -55,6 +78,8 @@ const CommentSchema = new Schema({ }, parent_id: String, + tags: [TagSchema], + // Additional metadata stored on the field. metadata: { default: {}, diff --git a/models/setting.js b/models/setting.js index e63fa8f3e..44f5ad5ca 100644 --- a/models/setting.js +++ b/models/setting.js @@ -1,6 +1,41 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; +const PERMISSIONS = [ + 'ADMIN' +]; + +/** + * The Mongo schema for a Tag. + * @type {Schema} + */ +const TagSchema = new Schema({ + id: { + type: String, + unique: true, + default: 'STAFF' + }, + public: Boolean, + text: { + type: Schema.Types.Mixed, + default: null + }, + permissions: [{ + type: String, + enum: PERMISSIONS, + default: 'ADMIN' + }], + models: [String], + + // Additional metadata stored on the field. + metadata: Schema.Types.Mixed +}, { + timestamps: { + createdAt: 'created_at', + updatedAt: 'updated_at' + } +}); + const MODERATION_OPTIONS = [ 'PRE', 'POST' @@ -88,7 +123,8 @@ const SettingSchema = new Schema({ type: Array, default: ['localhost'] } - } + }, + tags: [TagSchema] }, { timestamps: { createdAt: 'created_at', diff --git a/models/tag.js b/models/tag.js deleted file mode 100644 index 85bb0845a..000000000 --- a/models/tag.js +++ /dev/null @@ -1,77 +0,0 @@ -const mongoose = require('../services/mongoose'); -const uuid = require('uuid'); -const Schema = mongoose.Schema; - -// in settings -// --> decide who can apply them (self, role, anyone) and - -// Who can see the tag (self, by role, anyone) -const PRIVACY_TYPES = [ - 'ADMIN', - 'SELF', - 'PUBLIC' -]; - -// The type of item that the tag is apply on. -const ITEM_TYPES = [ - 'ASSETS', - 'COMMENTS', - 'USERS' -]; - -/** - * The Mongo schema for a Comment Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - id: { - type: String, - default: uuid.v4, - unique: true - }, - - name: { - type: String, - }, - - item_type: { - type: String, - enum: ITEM_TYPES - }, - - item_id: String, - - // The User ID of the user that assigned the status. - assigned_by: { - type: String, - default: null - }, - - privacy_type: { - type: String, - enum: PRIVACY_TYPES, - default: 'SELF' - }, - - // Additional metadata stored on the field. - metadata: Schema.Types.Mixed -}, { - timestamps: { - createdAt: 'created_at', - updatedAt: 'updated_at' - } -}); - -// Add the indixies on the tag on an item. -TagSchema.index({ - 'item_type': 1, - 'item_id': 1, - 'name': 1 -}, { - unique: true, - background: false -}); - -const Tag = mongoose.model('Tag', TagSchema); - -module.exports = Tag; diff --git a/models/user.js b/models/user.js index 159407fdf..b85f09a93 100644 --- a/models/user.js +++ b/models/user.js @@ -111,6 +111,18 @@ const UserSchema = new mongoose.Schema({ default: false }, + tags: [{ + id: { + type: String, + unique: true + }, + public: Boolean, + text: [{ + type: mongoose.Schema.Types.Mixed, + default: null + }] + }], + // User's settings settings: { bio: { diff --git a/services/comments.js b/services/comments.js index bdc878ce1..4c8f423c6 100644 --- a/services/comments.js +++ b/services/comments.js @@ -3,8 +3,7 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const TagsService = require('./tags'); -const TagModel = require('../models/tag'); +const SettingsService = require('./settings'); const STATUSES = [ 'ACCEPTED', @@ -13,6 +12,10 @@ const STATUSES = [ 'NONE', ]; +const ALLOWED_TAGS = [ + 'STAFF' +]; + module.exports = class CommentsService { /** @@ -22,6 +25,8 @@ module.exports = class CommentsService { */ static publicCreate(comment) { + console.log('-----------------> debug publicCreate'); + // Check to see if this is an array of comments, if so map it out. if (Array.isArray(comment)) { return Promise.all(comment.map(CommentsService.publicCreate)); @@ -47,22 +52,26 @@ module.exports = class CommentsService { * @throws if tag name is not in ALLOWED_TAGS * @param {String} id the id of the comment to tag * @param {String} name the name of the tag to add - * @param {String} assigned_by the user id for the user who added the tag + * @param {String} added_by the user id for the user who added the tag */ - static addTag(id, name, assigned_by, privacy_type) { + static addTag(id, name, added_by) { - return CommentModel.findOne({id}) - .then((comment) => { - if (comment == null) { + console.log('-----------------> debug addtag', name); + + SettingsService.retrieve() + .then(({tags}) => { + if (!ALLOWED_TAGS.includes(name) || tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { return Promise.reject(new Error('tag not allowed')); } - return TagsService.insertCommentTag({ - name, - item_id: id, - item_type: 'COMMENTS', - user_id: assigned_by, - privacy_type - }); + }); + + return CommentModel.findOneAndUpdate({id}, { + $push: { + tags: { + id: name, + added_by: added_by + } + } }); } @@ -70,17 +79,14 @@ module.exports = class CommentsService { * Removes a tag from a comment * @throws if the tag is not on the comment * @param {String} id the id of the comment to tag - * @param {String} name the name of the tag to add + * @param {String} tag_id the id of the tag to remove */ - static removeTag(id, name) { - return TagModel.findOneAndRemove({ - item_type: 'COMMENTS', - item_id: id, - name - }) - .then((tag) => { - if (tag == null) { - return Promise.reject(new Error('tag does not exist')); + static removeTag(id, tag_id) { + return CommentModel.findOneAndUpdate({id}, { + $pull: { + tags: { + id: tag_id + } } }); } diff --git a/services/mongoose.js b/services/mongoose.js index 206075239..c7092326f 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -65,5 +65,4 @@ require('../models/action'); require('../models/asset'); require('../models/comment'); require('../models/setting'); -require('../models/tag'); require('../models/user'); diff --git a/services/tags.js b/services/tags.js deleted file mode 100644 index 5cd0a09e1..000000000 --- a/services/tags.js +++ /dev/null @@ -1,83 +0,0 @@ -const TagModel = require('../models/tag'); - -const ALLOWED_COMMENT_TAGS = [ - {name: 'STAFF'}, - {name: 'BEST'}, -]; - -const ALLOWED_PRIVACY_TYPE = [ - {privacy_type: 'PUBLIC'}, - {privacy_type: 'SELF'}, - {privacy_type: 'ADMIN'} -]; - -module.exports = class TagsService { - - /** - * Finds a tag by the id. - * @param {String} id identifier of the tag (uuid) - */ - static findById(id) { - return TagModel.findOne({id}); - } - - /** - * Finds atag by the item_id and name. - * @param {String} item_id identifier of the item that the tag was applied into(uuid) - * @param {string} name name of the tag - */ - static findByItemIdAndName(item_id, name, item_type) { - return TagModel.find({ - item_id, - item_type, - name - }); - } - - /** - * Finds actions in an array of ids. - * @param {String} ids array of user identifiers (uuid) - */ - static async findByItemIdArray(item_ids) { - let tags = await TagModel.find({ - 'item_id': {$in: item_ids} - }); - - if (tags === null) { - return []; - } - - return tags; - } - - /** - * Add a tag. - * @param {string} name the actual tag - * @param {String} item_id identifier of the comment (uuid) - * @param {String} item_type type of the object being tag (COMMENTS) - * @param {String} user_id user id that assigned the tag (uuid) - * @param {String} privacy_type visibility of the tag on the comment - * @return {Promise} - */ - static insertCommentTag(tag) { - - if (ALLOWED_COMMENT_TAGS.find((t) => t.name === tag.name) == null) { - return Promise.reject(new Error('tag not allowed')); - } - - if (ALLOWED_PRIVACY_TYPE.find((p) => p.privacy_type === tag.privacy_type) == null) { - return Promise.reject(new Error('privacy type not allowed')); - } - - // Create/Update the tag. - let newtag = new TagModel({ - name: tag.name, - item_id: tag.item_id, - item_type: tag.item_type, - assigned_by: tag.user_id, - privacy_type: tag.privacy_type - }); - return newtag.save(); - } - -}; diff --git a/test/server/graph/loaders/metrics.js b/test/server/graph/loaders/metrics.js index a24c18f79..5d74f154d 100644 --- a/test/server/graph/loaders/metrics.js +++ b/test/server/graph/loaders/metrics.js @@ -23,11 +23,27 @@ describe('graph.loaders.Metrics', () => { describe('different comment states', () => { - beforeEach(() => CommentModel.create([ - {id: '1', body: 'a new comment!'}, - {id: '2', body: 'a new comment!'}, - {id: '3', body: 'a new comment!'} - ])); + beforeEach(() =>{ + CommentModel.create( {id: '1', body: 'a new comment!'}) + .then((comment) => { + console.log('*************** wtf comment', comment); + }) + .catch((error) => { + console.log('1 debug the rror create ', error); + }); + console.log('debug 1'); + CommentModel.create({id: '2', body: 'a new comment!'}) + .catch((error) => { + console.log('2 debug the rror create ', error); + }); + console.log('debug 2'); + CommentModel.create({id: '3', body: 'a new comment!'}) + .catch((error) => { + console.log('3 debug the rror create ', error); + }); + console.log('debug 3'); + } + ); [ {flagged: 0, actions: []}, @@ -40,6 +56,7 @@ describe('graph.loaders.Metrics', () => { ]} ].forEach(({flagged, actions}) => { + console.log('-----------------> debug forEach'); describe(`with actions=${actions.length}`, () => { beforeEach(() => ActionModel.create(actions)); @@ -52,7 +69,6 @@ describe('graph.loaders.Metrics', () => { to: (new Date()).setMinutes((new Date()).getMinutes() + 5) }) .then(({data, errors}) => { - console.log(errors); expect(errors).to.be.undefined; expect(data.flagged).to.have.length(flagged); }); diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 01966bd88..83a12dc2e 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -4,7 +4,6 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); -const TagService = require('../../../../services/tags'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -16,8 +15,8 @@ describe('graph.mutations.addCommentTag', () => { }); const query = ` - mutation AddCommentTag ($id: ID!, $tag: String!, $privacy_type: String!) { - addCommentTag(id:$id, tag:$tag, privacy_type:$privacy_type) { + mutation AddCommentTag ($id: ID!, $tag: String!) { + addCommentTag(id:$id, tag:$tag) { comment { id } @@ -31,14 +30,14 @@ describe('graph.mutations.addCommentTag', () => { it('moderators can add tags to comments', async () => { const user = new UserModel({roles: ['MODERATOR' ]}); const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST', privacy_type: 'PUBLIC'}); + const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); } expect(response.errors).to.be.empty; - return TagService.findByItemIdAndName(response.data.addCommentTag.comment.id, 'BEST', 'COMMENTS') - .then((tags) => { + return CommentsService.findById(response.data.addCommentTag.comment.id) + .then(({tags}) => { expect(tags).to.have.length(1); }); }); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index d132c46c7..31cb9872f 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -9,7 +9,7 @@ const AssetModel = require('../../../../models/asset'); const ActionModel = require('../../../../models/action'); const SettingsService = require('../../../../services/settings'); -const TagService = require('../../../../services/tags'); +const CommentsService = require('../../../../services/comments'); describe('graph.mutations.createComment', () => { beforeEach(() => SettingsService.init()); @@ -223,9 +223,9 @@ describe('graph.mutations.createComment', () => { expect(data.createComment).to.have.property('comment').not.null; expect(data.createComment).to.have.property('errors').null; - return TagService.findByItemIdAndName(data.createComment.comment.id, tag, 'COMMENTS'); + return CommentsService.findById(data.createComment.comment.id); }) - .then((tags) => { + .then(({tags}) => { if (tag) { expect(tags).to.have.length(1); expect(tags[0]).to.have.property('name', tag); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 7660eef35..4deb4bd5e 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -7,7 +7,6 @@ const UserModel = require('../../../../models/user'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); -const TagService = require('../../../../services/tags'); describe('graph.mutations.removeCommentTag', () => { let comment; @@ -42,8 +41,8 @@ describe('graph.mutations.removeCommentTag', () => { expect(response.errors).to.be.empty; expect(response.data.removeCommentTag.errors).to.be.null; - TagService.findByItemIdAndName(response.data.removeCommentTag.comment.id, 'BEST') - .then((tags) => { + CommentsService.findById(response.data.removeCommentTag.comment.id) + .then(({tags}) => { expect(tags).to.deep.equal([]); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index d4fc5f17e..ed908d1b2 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -5,7 +5,6 @@ const ActionsService = require('../../../services/actions'); const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const CommentsService = require('../../../services/comments'); -const TagService = require('../../../services/tags'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; @@ -226,7 +225,7 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); - const tags = await TagService.findByItemIdAndName(commentId, 'BEST', 'COMMENTS'); + const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); expect(tags[0].name).to.equal(tagName); expect(tags[0].assigned_by).to.equal(userId); @@ -257,13 +256,13 @@ describe('services.CommentsService', () => { const commentId = comments[0].id; const tagName = 'BEST'; await CommentsService.addTag(commentId, tagName, users[0].id, 'PUBLIC'); - const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); // ok now to remove it await CommentsService.removeTag(commentId, tagName); - const tags2 = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); - expect(tags2.length).to.equal(0); + const comment = await CommentsService.findById(commentId); + expect(comment.tags.length).to.equal(0); }); it('throws if removing a tag that isn\'t there', async () => { const commentId = comments[0].id; @@ -271,7 +270,7 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; // just make sure it has no tags to start - const tags = await TagService.findByItemIdAndName(commentId, tagName, 'COMMENTS'); + const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(0); // ok now to remove it From d7e72bb1387e1732ee6a543d88efca89f029b0ce Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 5 May 2017 14:09:00 -0700 Subject: [PATCH 008/264] Fix tests. --- graph/mutators/comment.js | 4 +- services/comments.js | 39 ++++++++++++-------- test/server/graph/loaders/metrics.js | 25 ++----------- test/server/graph/mutations/createComment.js | 4 +- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 3413c6f4e..427ab4c63 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id); + await CommentsService.addTag(comment.id, 'STAFF', user); } // If the loaders are present, clear the caches for these values because we @@ -209,7 +209,7 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { * @param {String} tag name of the tag */ const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user.id); + return CommentsService.addTag(id, tag, user); }; /** diff --git a/services/comments.js b/services/comments.js index 4c8f423c6..41c383725 100644 --- a/services/comments.js +++ b/services/comments.js @@ -2,7 +2,7 @@ const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const ActionsService = require('./actions'); - +const SettingModel = require('../models/setting'); const SettingsService = require('./settings'); const STATUSES = [ @@ -25,8 +25,6 @@ module.exports = class CommentsService { */ static publicCreate(comment) { - console.log('-----------------> debug publicCreate'); - // Check to see if this is an array of comments, if so map it out. if (Array.isArray(comment)) { return Promise.all(comment.map(CommentsService.publicCreate)); @@ -56,22 +54,33 @@ module.exports = class CommentsService { */ static addTag(id, name, added_by) { - console.log('-----------------> debug addtag', name); - - SettingsService.retrieve() - .then(({tags}) => { - if (!ALLOWED_TAGS.includes(name) || tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { + // Check that the tag is allowed by the system OR in our setting.tags. + return SettingsService.retrieve() + .then((settings) => { + + // Moderators or ADMIN can add any tag automatically. + if (added_by != null && (added_by.hasRoles('ADMIN') || added_by.hasRoles('MODERATOR'))) { + SettingModel.findOneAndUpdate({id: settings.id}, { + $push: { + tags: { + id: name, + added_by: added_by.id + } + } + }); + } + else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { return Promise.reject(new Error('tag not allowed')); } - }); - return CommentModel.findOneAndUpdate({id}, { - $push: { - tags: { - id: name, - added_by: added_by + return CommentModel.findOneAndUpdate({id}, { + $push: { + tags: { + id: name, + added_by: added_by.id + } } - } + }); }); } diff --git a/test/server/graph/loaders/metrics.js b/test/server/graph/loaders/metrics.js index 5d74f154d..0d2741ef8 100644 --- a/test/server/graph/loaders/metrics.js +++ b/test/server/graph/loaders/metrics.js @@ -23,27 +23,11 @@ describe('graph.loaders.Metrics', () => { describe('different comment states', () => { - beforeEach(() =>{ - CommentModel.create( {id: '1', body: 'a new comment!'}) - .then((comment) => { - console.log('*************** wtf comment', comment); - }) - .catch((error) => { - console.log('1 debug the rror create ', error); - }); - console.log('debug 1'); - CommentModel.create({id: '2', body: 'a new comment!'}) - .catch((error) => { - console.log('2 debug the rror create ', error); - }); - console.log('debug 2'); + beforeEach(() =>[ + CommentModel.create( {id: '1', body: 'a new comment!'}), + CommentModel.create({id: '2', body: 'a new comment!'}), CommentModel.create({id: '3', body: 'a new comment!'}) - .catch((error) => { - console.log('3 debug the rror create ', error); - }); - console.log('debug 3'); - } - ); + ]); [ {flagged: 0, actions: []}, @@ -56,7 +40,6 @@ describe('graph.loaders.Metrics', () => { ]} ].forEach(({flagged, actions}) => { - console.log('-----------------> debug forEach'); describe(`with actions=${actions.length}`, () => { beforeEach(() => ActionModel.create(actions)); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index 31cb9872f..41822fcdb 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -21,7 +21,7 @@ describe('graph.mutations.createComment', () => { id status tags { - name + id } } errors { @@ -228,7 +228,7 @@ describe('graph.mutations.createComment', () => { .then(({tags}) => { if (tag) { expect(tags).to.have.length(1); - expect(tags[0]).to.have.property('name', tag); + expect(tags[0]).to.have.property('id', tag); } else { expect(tags).length(0); } From c72cb2428e241dd6b97790ff22542d2b26bb137c Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 5 May 2017 14:33:02 -0700 Subject: [PATCH 009/264] Fixed errors on tags creation --- services/comments.js | 4 ++-- test/server/graph/mutations/removeCommentTag.js | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/services/comments.js b/services/comments.js index 41c383725..aae5c8248 100644 --- a/services/comments.js +++ b/services/comments.js @@ -57,14 +57,14 @@ module.exports = class CommentsService { // Check that the tag is allowed by the system OR in our setting.tags. return SettingsService.retrieve() .then((settings) => { - + // Moderators or ADMIN can add any tag automatically. if (added_by != null && (added_by.hasRoles('ADMIN') || added_by.hasRoles('MODERATOR'))) { SettingModel.findOneAndUpdate({id: settings.id}, { $push: { tags: { id: name, - added_by: added_by.id + models: ['COMMENTS'] } } }); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 4deb4bd5e..663e0de9a 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -4,6 +4,7 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); +const SettingModel = require('../../../../models/setting'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); @@ -33,7 +34,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user.id, 'PUBLIC'); + await CommentsService.addTag(comment.id, 'BEST', user); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); @@ -49,6 +50,16 @@ describe('graph.mutations.removeCommentTag', () => { }); describe('users who cant remove tags', () => { + + // allow the tag in the settings + SettingModel.findOneAndUpdate({id: 1}, { + $push: { + tags: { + id: 'BEST', + models: ['COMMENTS'] + } + } + }); Object.entries({ 'anonymous': undefined, 'regular commenter': new UserModel({}), @@ -58,7 +69,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user ? user.id : null, 'PUBLIC'); + await CommentsService.addTag(comment.id, 'BEST', user); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); From 607ab6c0d6c998542a64746eebd3ff88fb409e86 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 5 May 2017 13:14:34 -0600 Subject: [PATCH 010/264] Added privacy fix --- models/user.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/models/user.js b/models/user.js index 159407fdf..95a1422f2 100644 --- a/models/user.js +++ b/models/user.js @@ -136,6 +136,14 @@ const UserSchema = new mongoose.Schema({ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' + }, + + toJSON: { + transform: function (doc, ret) { + delete ret.password; + delete ret._id; + delete ret.__v; + } } }); From 85fae3104e475889cfd85316813f642e66bfc0f6 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 5 May 2017 14:05:52 -0600 Subject: [PATCH 011/264] float: right; in css is just fine. --- .../coral-configure/components/ConfigureCommentStream.css | 6 ++---- client/coral-configure/components/ConfigureCommentStream.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-configure/components/ConfigureCommentStream.css index 335c94377..a8cbc36cb 100644 --- a/client/coral-configure/components/ConfigureCommentStream.css +++ b/client/coral-configure/components/ConfigureCommentStream.css @@ -3,10 +3,8 @@ } .apply { - position: absolute; - top: 38%; - transform: translateX(-50%); - right: 0; + float: right; + margin: 0 10px; } .wrapper ul { diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index bef53e1ef..6c00b5996 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -12,7 +12,6 @@ export default ({handleChange, handleApply, changed, ...props}) => (

{lang.t('configureCommentStream.title')}

-

{lang.t('configureCommentStream.description')}

+

{lang.t('configureCommentStream.description')}

  • From c4c0ee79cd48a3972bb2f2e8e1d690131b48c9ca Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 12:57:37 -0700 Subject: [PATCH 012/264] Fix tests. --- errors.js | 14 ++++++ graph/mutators/comment.js | 4 +- models/user.js | 42 +++++++++++------- services/comments.js | 43 ++++++++++++------- services/users.js | 1 + test/server/graph/mutations/ignoreUser.js | 11 +++++ .../graph/mutations/removeCommentTag.js | 39 +++++++++-------- test/server/services/comments.js | 6 +-- 8 files changed, 106 insertions(+), 54 deletions(-) diff --git a/errors.js b/errors.js index 67ae0693a..9f12c7835 100644 --- a/errors.js +++ b/errors.js @@ -75,6 +75,18 @@ const ErrMissingToken = new APIError('token is required', { status: 400 }); +// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. +const ErrNoCommentFound = new APIError('comment does not exist', { + translation_key: 'COMMENT_NOT_FOUND', + status: 400 +}); + +// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. +const ErrorTagNotAllowed = new APIError('tag not allowed', { + translation_key: 'TAG_NOT_ALLOWED', + status: 400 +}); + // ErrAssetCommentingClosed is returned when a comment or action is attempted on // a stream where commenting has been closed. class ErrAssetCommentingClosed extends APIError { @@ -161,6 +173,8 @@ module.exports = { ErrMissingEmail, ErrMissingPassword, ErrMissingToken, + ErrNoCommentFound, + ErrorTagNotAllowed, ErrEmailTaken, ErrSpecialChars, ErrMissingUsername, diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 427ab4c63..3413c6f4e 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -28,7 +28,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par .then(async (comment) => { if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user); + await CommentsService.addTag(comment.id, 'STAFF', user.id); } // If the loaders are present, clear the caches for these values because we @@ -209,7 +209,7 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { * @param {String} tag name of the tag */ const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user); + return CommentsService.addTag(id, tag, user.id); }; /** diff --git a/models/user.js b/models/user.js index 73e04c392..688c37c2c 100644 --- a/models/user.js +++ b/models/user.js @@ -1,5 +1,6 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); +const Schema = mongoose.Schema; const uuid = require('uuid'); // USER_ROLES is the array of roles that is permissible as a user role. @@ -16,9 +17,32 @@ const USER_STATUS = [ 'APPROVED' // Indicates that the users' username has been approved ]; +// /** +// * The Mongo schema for a User Tag. +// * @type {Schema} +// */ +// const TagSchema = new Schema({ +// +// // This is the actual 'tag' and we only permit tags that are in Setting.tags. +// id: String, +// +// // The User ID of the user that added the tag. +// added_by: { +// type: String, +// default: null +// }, +// +// created_at: { +// type: Date, +// default: Date +// } +// }, { +// _id: false +// }); + // ProfileSchema is the mongoose schema defined as the representation of a // User's profile stored in MongoDB. -const ProfileSchema = new mongoose.Schema({ +const ProfileSchema = new Schema({ // ID provides the identifier for the user profile, in the case of a local // provider, the id would be an email, in the case of a social provider, @@ -41,7 +65,7 @@ const ProfileSchema = new mongoose.Schema({ // used by the `local` provider to indicate when the email address was // confirmed. metadata: { - type: mongoose.Schema.Types.Mixed + type: Schema.Types.Mixed } }, { _id: false @@ -49,7 +73,7 @@ const ProfileSchema = new mongoose.Schema({ // UserSchema is the mongoose schema defined as the representation of a User in // MongoDB. -const UserSchema = new mongoose.Schema({ +const UserSchema = new Schema({ // This ID represents the most unique identifier for a user, it is generated // when the user is created as a random uuid. @@ -111,18 +135,6 @@ const UserSchema = new mongoose.Schema({ default: false }, - tags: [{ - id: { - type: String, - unique: true - }, - public: Boolean, - text: [{ - type: mongoose.Schema.Types.Mixed, - default: null - }] - }], - // User's settings settings: { bio: { diff --git a/services/comments.js b/services/comments.js index aae5c8248..db4dd18c1 100644 --- a/services/comments.js +++ b/services/comments.js @@ -4,6 +4,9 @@ const ActionModel = require('../models/action'); const ActionsService = require('./actions'); const SettingModel = require('../models/setting'); const SettingsService = require('./settings'); +const UsersService = require('./users'); + +const errors = require('../errors'); const STATUSES = [ 'ACCEPTED', @@ -58,29 +61,37 @@ module.exports = class CommentsService { return SettingsService.retrieve() .then((settings) => { - // Moderators or ADMIN can add any tag automatically. - if (added_by != null && (added_by.hasRoles('ADMIN') || added_by.hasRoles('MODERATOR'))) { - SettingModel.findOneAndUpdate({id: settings.id}, { - $push: { - tags: { - id: name, - models: ['COMMENTS'] + UsersService.findById(added_by) + .then((user) => { + + // Moderators or ADMIN can add any tag automatically. + if (user != null && (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { + SettingModel.findOneAndUpdate({id: settings.id}, { + $push: { + tags: { + id: name, + models: ['COMMENTS'] + } } - } - }); - } - else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { - return Promise.reject(new Error('tag not allowed')); - } + }); + } + else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { + return Promise.reject(errors.ErrorTagNotAllowed); + } + }); return CommentModel.findOneAndUpdate({id}, { $push: { tags: { id: name, - added_by: added_by.id + added_by: added_by } - } - }); + }, + }, + { + new: false, + upsert: false + }); }); } diff --git a/services/users.js b/services/users.js index 14301ccf4..c33b18780 100644 --- a/services/users.js +++ b/services/users.js @@ -339,6 +339,7 @@ module.exports = class UsersService { if (err.message.match('Username')) { return reject(errors.ErrUsernameTaken); } + console.log('DEBUG FUCKING ERROR', err); return reject(errors.ErrEmailTaken); } return reject(err); diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 54d6305a1..94243a690 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -32,11 +32,18 @@ describe('graph.mutations.ignoreUser', () => { // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { + UsersService.findLocalUser('usernameB@example.com') + .then((user) => { + console.log('--------- debug user', user); + }); const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + console.log('--------------- debug aca -2'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); + console.log('--------------- debug aca -1'); const context = new Context({user}); const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: userToIgnore.id}); if (ignoreUserResponse.errors && ignoreUserResponse.errors.length) { + console.log('--------------- debug aca 0'); console.error(ignoreUserResponse.errors); } expect(ignoreUserResponse.errors).to.be.empty; @@ -44,13 +51,17 @@ describe('graph.mutations.ignoreUser', () => { // now check my ignored users const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { + console.log('debug aca 1'); console.error(myIgnoredUsersResponse.errors); } expect(myIgnoredUsersResponse.errors).to.be.empty; const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; expect(myIgnoredUsers.length).to.equal(1); + console.log('debug aca 2'); expect(myIgnoredUsers[0].id).to.equal(userToIgnore.id); + console.log('debug aca 3'); expect(myIgnoredUsers[0].username).to.equal(userToIgnore.username); + console.log('debug aca 4'); }); it('users cannot ignore themselves', async () => { diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 663e0de9a..760887a44 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -34,7 +34,7 @@ describe('graph.mutations.removeCommentTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user); + await CommentsService.addTag(comment.id, 'BEST', user.id); const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); @@ -59,25 +59,28 @@ describe('graph.mutations.removeCommentTag', () => { models: ['COMMENTS'] } } - }); - Object.entries({ - 'anonymous': undefined, - 'regular commenter': new UserModel({}), - 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) - }).forEach(([ userDescription, user ]) => { - it(userDescription, async function () { - const context = new Context({user}); + }) + .then(() => { + Object.entries({ + 'anonymous': undefined, + 'regular commenter': new UserModel({}), + 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) + }).forEach(([ userDescription, user ]) => { + it(userDescription, async function () { + const context = new Context({user}); - // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); - if (response.errors && response.errors.length) { - console.error(response.errors); - } - expect(response.errors).to.be.empty; + // add a tag first + await CommentsService.addTag(comment.id, 'BEST', user.id); - expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); - expect(response.data.removeCommentTag.comment).to.be.null; + const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); + if (response.errors && response.errors.length) { + console.error(response.errors); + } + expect(response.errors).to.be.empty; + + expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); + expect(response.data.removeCommentTag.comment).to.be.null; + }); }); }); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index ed908d1b2..e01fc7bad 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -227,8 +227,8 @@ describe('services.CommentsService', () => { await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); - expect(tags[0].name).to.equal(tagName); - expect(tags[0].assigned_by).to.equal(userId); + expect(tags[0].id).to.equal(tagName); + expect(tags[0].added_by).to.equal(userId); expect(tags[0].created_at).to.be.an.instanceof(Date); }); it('can\'t add a tag to comment id that doesn\'t exist', async () => { @@ -238,7 +238,7 @@ describe('services.CommentsService', () => { await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; }); - it('can\'t add same tag.name twice', async () => { + it('can\'t add same tag.id twice', async () => { const commentId = comments[0].id; const tagName = 'BEST'; const userId = users[0].id; From 743a96cd217f7be2790658ca9e7455d6ece4ff81 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 13:09:17 -0700 Subject: [PATCH 013/264] Removes debug statements. --- services/users.js | 1 - test/server/graph/mutations/ignoreUser.js | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/services/users.js b/services/users.js index c33b18780..14301ccf4 100644 --- a/services/users.js +++ b/services/users.js @@ -339,7 +339,6 @@ module.exports = class UsersService { if (err.message.match('Username')) { return reject(errors.ErrUsernameTaken); } - console.log('DEBUG FUCKING ERROR', err); return reject(errors.ErrEmailTaken); } return reject(err); diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 94243a690..8de54a41d 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -32,18 +32,12 @@ describe('graph.mutations.ignoreUser', () => { // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { - UsersService.findLocalUser('usernameB@example.com') - .then((user) => { - console.log('--------- debug user', user); - }); + UsersService.findLocalUser('usernameB@example.com'); const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); - console.log('--------------- debug aca -2'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); - console.log('--------------- debug aca -1'); const context = new Context({user}); const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: userToIgnore.id}); if (ignoreUserResponse.errors && ignoreUserResponse.errors.length) { - console.log('--------------- debug aca 0'); console.error(ignoreUserResponse.errors); } expect(ignoreUserResponse.errors).to.be.empty; @@ -51,17 +45,13 @@ describe('graph.mutations.ignoreUser', () => { // now check my ignored users const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { - console.log('debug aca 1'); console.error(myIgnoredUsersResponse.errors); } expect(myIgnoredUsersResponse.errors).to.be.empty; const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; expect(myIgnoredUsers.length).to.equal(1); - console.log('debug aca 2'); expect(myIgnoredUsers[0].id).to.equal(userToIgnore.id); - console.log('debug aca 3'); expect(myIgnoredUsers[0].username).to.equal(userToIgnore.username); - console.log('debug aca 4'); }); it('users cannot ignore themselves', async () => { From 96cd5197e3b5b0804ccfc28d27edca6c332b3377 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 16:05:40 -0700 Subject: [PATCH 014/264] Test was failing as I was not returning an error. --- errors.js | 4 ++-- models/comment.js | 9 +++++++ services/comments.js | 29 +++++++++++++++++------ test/server/graph/mutations/ignoreUser.js | 1 - test/server/services/comments.js | 11 +++++---- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/errors.js b/errors.js index 9f12c7835..5c2b05f92 100644 --- a/errors.js +++ b/errors.js @@ -82,7 +82,7 @@ const ErrNoCommentFound = new APIError('comment does not exist', { }); // ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. -const ErrorTagNotAllowed = new APIError('tag not allowed', { +const ErrTagNotAllowed = new APIError('tag not allowed', { translation_key: 'TAG_NOT_ALLOWED', status: 400 }); @@ -174,7 +174,7 @@ module.exports = { ErrMissingPassword, ErrMissingToken, ErrNoCommentFound, - ErrorTagNotAllowed, + ErrTagNotAllowed, ErrEmailTaken, ErrSpecialChars, ErrMissingUsername, diff --git a/models/comment.js b/models/comment.js index f3e239080..219ec055d 100644 --- a/models/comment.js +++ b/models/comment.js @@ -92,6 +92,15 @@ const CommentSchema = new Schema({ } }); +// Add the indexes on the comment tag. +CommentSchema.index({ + 'id': 1, + 'tags.id': 1 +}, { + unique: true, + background: false +}); + // Comment model. const Comment = mongoose.model('Comment', CommentSchema); diff --git a/services/comments.js b/services/comments.js index db4dd18c1..e1873ba02 100644 --- a/services/comments.js +++ b/services/comments.js @@ -76,21 +76,26 @@ module.exports = class CommentsService { }); } else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { - return Promise.reject(errors.ErrorTagNotAllowed); + return Promise.reject(errors.ErrTagNotAllowed); } }); - return CommentModel.findOneAndUpdate({id}, { + return CommentModel.findOneAndUpdate({id, 'tags.id': {$ne: name}}, { $push: { tags: { id: name, added_by: added_by } }, - }, - { - new: false, - upsert: false + }) + .then(({nModified}) => { + switch (nModified) { + case 0: + return Promise.reject(errors.ErrNoCommentFound); + case 1: + return; + default: + } }); }); } @@ -102,12 +107,22 @@ module.exports = class CommentsService { * @param {String} tag_id the id of the tag to remove */ static removeTag(id, tag_id) { - return CommentModel.findOneAndUpdate({id}, { + return CommentModel.findOneAndUpdate({id, 'tags.id': tag_id}, { $pull: { tags: { id: tag_id } } + } + ) + .then(({nModified}) => { + switch(nModified) { + case 0: + return Promise.reject(errors.ErrNoCommentFound); + case 1: + return; + default: + } }); } diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 8de54a41d..54d6305a1 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -32,7 +32,6 @@ describe('graph.mutations.ignoreUser', () => { // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { - UsersService.findLocalUser('usernameB@example.com'); const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); const context = new Context({user}); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index e01fc7bad..27657efa5 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -236,7 +236,10 @@ describe('services.CommentsService', () => { const tagName = 'BEST'; const userId = users[0].id; - await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; + CommentsService.addTag(commentId, tagName, userId) + .catch((error) => { + expect(error).to.not.be.null; + }); }); it('can\'t add same tag.id twice', async () => { const commentId = comments[0].id; @@ -244,10 +247,10 @@ describe('services.CommentsService', () => { const userId = users[0].id; // first time - await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); + await CommentsService.addTag(commentId, tagName, userId); // second time should fail - await expect(CommentsService.addTag(commentId, tagName, userId, 'PUBLIC')).to.be.rejected; + await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; }); }); @@ -255,7 +258,7 @@ describe('services.CommentsService', () => { it('removes a tag', async () => { const commentId = comments[0].id; const tagName = 'BEST'; - await CommentsService.addTag(commentId, tagName, users[0].id, 'PUBLIC'); + await CommentsService.addTag(commentId, tagName, users[0].id); const {tags} = await CommentsService.findById(commentId); expect(tags.length).to.equal(1); From 7c09902294e7c9217ed751548606735c63a509ac Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 16:55:11 -0700 Subject: [PATCH 015/264] The name for the tag is called ID. --- .../src/containers/Comment.js | 2 +- .../graphql/fragments/commentView.graphql | 2 +- .../graphql/mutations/addCommentTag.graphql | 2 +- .../graphql/mutations/removeCommentTag.graphql | 2 +- graph/loaders/settings.js | 18 ++---------------- 5 files changed, 6 insertions(+), 20 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 9b48e0809..09723b973 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -28,7 +28,7 @@ export default withFragments({ created_at status tags { - name + id } user { id diff --git a/client/coral-framework/graphql/fragments/commentView.graphql b/client/coral-framework/graphql/fragments/commentView.graphql index 0ed5e00b8..71a6ae2d4 100644 --- a/client/coral-framework/graphql/fragments/commentView.graphql +++ b/client/coral-framework/graphql/fragments/commentView.graphql @@ -6,7 +6,7 @@ fragment commentView on Comment { created_at status tags { - name + id } user { id diff --git a/client/coral-framework/graphql/mutations/addCommentTag.graphql b/client/coral-framework/graphql/mutations/addCommentTag.graphql index 5fd63868e..a4ff36da4 100644 --- a/client/coral-framework/graphql/mutations/addCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/addCommentTag.graphql @@ -3,7 +3,7 @@ mutation AddCommentTag ($id: ID!, $tag: String!) { comment { id tags { - name + id } } errors { diff --git a/client/coral-framework/graphql/mutations/removeCommentTag.graphql b/client/coral-framework/graphql/mutations/removeCommentTag.graphql index 3826b0703..642466f00 100644 --- a/client/coral-framework/graphql/mutations/removeCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/removeCommentTag.graphql @@ -3,7 +3,7 @@ mutation RemoveCommentTag ($id: ID!, $tag: String!) { comment { id tags { - name + id } } errors { diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js index 9d777255f..83545821d 100644 --- a/graph/loaders/settings.js +++ b/graph/loaders/settings.js @@ -1,25 +1,11 @@ const SettingsService = require('../../services/settings'); -const SettingModel = require('../../models/setting'); - const util = require('./util'); -/** - * Search for tags based on their item_type. - * @param {String} item_type the item type to search by - * @return {Promise} resolves to distinct items tags - */ -const getByItemType = (_, item_type) => { - return SettingModel.distinct('tags.models', {item_type}); -}; - /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request * @return {Object} object of loaders */ -module.exports = (context) => ({ - Settings: { - get: new util.SingletonResolver(() => SettingsService.retrieve()), - getTagsByItemType: (item_type) => getByItemType(context, item_type) - } +module.exports = () => ({ + Settings: new util.SingletonResolver(() => SettingsService.retrieve()) }); From da0cebbda72b2e6b2e8335987c8dfc8179193ae7 Mon Sep 17 00:00:00 2001 From: gaba Date: Mon, 8 May 2017 17:06:49 -0700 Subject: [PATCH 016/264] Fix the plugin TagLabel to show the staff tag. --- client/coral-embed-stream/src/components/Comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 4349fe38d..b58bf2957 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -23,7 +23,7 @@ import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import styles from './Comment.css'; -const isStaff = tags => !tags.every(t => t.name !== 'STAFF'); +const isStaff = tags => !tags.every(t => t.id !== 'STAFF'); // hold actions links (e.g. Reply) along the comment footer const ActionButton = ({children}) => { @@ -72,7 +72,7 @@ class Comment extends React.Component { id: PropTypes.string.isRequired, tags: PropTypes.arrayOf( PropTypes.shape({ - name: PropTypes.string + id: PropTypes.string }) ), replies: PropTypes.arrayOf( From 2e1bc8d75a2b21f3a8ffac41653ae78ede367c6a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 15:01:01 -0600 Subject: [PATCH 017/264] Adjusted query/tag flow --- graph/mutators/comment.js | 155 ++++++++++++++++-- graph/resolvers/index.js | 4 + graph/resolvers/root_mutation.js | 11 +- graph/resolvers/tag.js | 5 + graph/resolvers/tag_link.js | 9 + graph/typeDefs.graphql | 36 ++-- models/action.js | 13 +- models/comment.js | 44 +---- models/enum/action_types.js | 4 + models/enum/comment_status.js | 6 + models/enum/item_types.js | 5 + models/enum/moderation_options.js | 4 + models/enum/user_roles.js | 4 + models/enum/user_status.js | 6 + models/schema/tag.js | 53 ++++++ models/schema/tag_link.js | 31 ++++ models/setting.js | 43 +---- models/user.js | 43 +---- services/comments.js | 122 +++++--------- services/tags.js | 5 + services/users.js | 4 +- test/server/graph/mutations/addCommentTag.js | 29 ++-- test/server/graph/mutations/createComment.js | 6 +- .../graph/mutations/removeCommentTag.js | 68 ++++---- test/server/services/comments.js | 109 +++++++----- 25 files changed, 476 insertions(+), 343 deletions(-) create mode 100644 graph/resolvers/tag_link.js create mode 100644 models/enum/action_types.js create mode 100644 models/enum/comment_status.js create mode 100644 models/enum/item_types.js create mode 100644 models/enum/moderation_options.js create mode 100644 models/enum/user_roles.js create mode 100644 models/enum/user_status.js create mode 100644 models/schema/tag.js create mode 100644 models/schema/tag_link.js create mode 100644 services/tags.js diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 3413c6f4e..3a57f3b3a 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -18,18 +18,25 @@ const Wordlist = require('../../services/wordlist'); */ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null}, status = 'NONE') => { + // Add the staff tag for comments created as a staff member. + let tags = []; + if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + tags = [{ + tag: { + name: 'STAFF' + } + }]; + } + return CommentsService.publicCreate({ body, asset_id, parent_id, status, + tags, author_id: user.id }) - .then(async (comment) => { - - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - await CommentsService.addTag(comment.id, 'STAFF', user.id); - } + .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. We should @@ -204,21 +211,139 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { }; /** - * Adds a tag to a Comment - * @param {String} id identifier of the comment (uuid) - * @param {String} tag name of the tag + * Adds a tag to a Comment. */ -const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.addTag(id, tag, user.id); +const addCommentTag = async ({user, loaders: {Assets, Comments, Settings}}, {id, asset_id, name}) => { + + // If the current user does not have the ADMIN or MODERATOR role + if (!user || !user.can('mutation:addCommentTag')) { + throw errors.ErrNotAuthorized; + } + + // Load the settings from the dataloader, it will be cached if it was + // retrieved already. + let settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); + + // Try to find the tag in the asset tags. This will contain the permission + // information. + let tag = settings.tags.find((globalTag) => { + return globalTag.name === name && globalTag.models.include('COMMENTS'); + }); + + // Create the new tagLink that will be created to attach to the comment. + let tagLink = { + tag, + assigned_by: user.id, + created_at: new Date() + }; + + // If the tag was found, we need to ensure that the current user can indeed + // set this tag on the comment. + if (tag) { + + // If the tag has roles defined, and the current user has at least one of + // the required roles, then add the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return CommentsService.addTag(id, tagLink, false); + } + + // If the permissions allow for self assignment, then ensure that the query + // is compose with that in mind. + if (tag.permissions && tag.permissions.self) { + + // Otherwise, we assume that we have to check to see that the user indeed + // owns the resource before allowing the tag to get added. + return CommentsService.addTag(id, tagLink, true); + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can add unique tags, these are tags that are not in + // the global list. + if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { + throw errors.ErrNotAuthorized; + } + + // Generate the tag in the event now that we have to create the tag for this + // specific comment. + tagLink.tag = { + name, + permissions: { + public: true, + self: false, + roles: [] + }, + models: ['COMMENTS'], + created_at: new Date() + }; + + // Actually attach the new tag that we created to the comment. + return CommentsService.addTag(id, tagLink, false); }; /** - * Removes a tag from a Comment - * @param {String} id identifier of the comment (uuid) - * @param {String} tag name of the tag + * Removes a tag from a Comment. */ -const removeCommentTag = ({user, loaders: {Comments}}, {id, tag}) => { - return CommentsService.removeTag(id, tag); +const removeCommentTag = async ({user, loaders: {Comments}}, {id, asset_id, name}) => { + + // If the current user does not have the ADMIN or MODERATOR role + if (!user || !user.can('mutation:removeCommentTag')) { + throw errors.ErrNotAuthorized; + } + + // Load the settings from the dataloader, it will be cached if it was + // retrieved already. + let settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); + + // Try to find the tag in the asset tags. This will contain the permission + // information. + let tag = settings.tags.find((globalTag) => { + return globalTag.name === name && globalTag.models.include('COMMENTS'); + }); + + // Create the new tagLink that will be created to attach to the comment. + let tagLink = { + tag, + assigned_by: user.id + }; + + // If the tag was found, we need to ensure that the current user can indeed + // remove this tag on the comment. + if (tag) { + + // If the tag has roles defined, and the current user has at least one of + // the required roles, then remove the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return CommentsService.removeTag(id, tagLink, false); + } + + // If the permissions allow for self assignment, then ensure that the query + // is compose with that in mind. + if (tag.permissions && tag.permissions.self) { + + // Otherwise, we assume that we have to check to see that the user indeed + // owns the resource before allowing the tag to get removed. + return CommentsService.removeTag(id, tagLink, true); + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can remove unique tags, these are tags that are not + // in the global list. + if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { + throw errors.ErrNotAuthorized; + } + + // Generate the tag in the event now that we have to create the tag for this + // specific comment. + tagLink.tag = { + name + }; + + // Actually attach the new tag that we created to the comment. + return CommentsService.removeTag(id, tagLink, false); }; module.exports = (context) => { diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index cbeb6dbf9..f2707bcc8 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -16,6 +16,8 @@ const RootMutation = require('./root_mutation'); const RootQuery = require('./root_query'); const Settings = require('./settings'); const Subscription = require('./subscription'); +const TagLink = require('./tag_link'); +const Tag = require('./tag'); const UserError = require('./user_error'); const User = require('./user'); const ValidationUserError = require('./validation_user_error'); @@ -39,6 +41,8 @@ let resolvers = { RootQuery, Settings, Subscription, + TagLink, + Tag, UserError, User, ValidationUserError, diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 5b60fbaa8..442845bd2 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -1,5 +1,4 @@ const wrapResponse = require('../helpers/response'); -const CommentsService = require('../../services/comments'); const RootMutation = { createComment(_, {comment}, {mutators: {Comment}}) { @@ -29,12 +28,12 @@ const RootMutation = { setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, - addCommentTag(_, {id, tag}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.addCommentTag({id, tag}).then(() => CommentsService.findById(id))); - }, - removeCommentTag(_, {id, tag}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id))); + addCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.addCommentTag({id, asset_id, name})); }, + removeCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { + return wrapResponse('comment')(Comment.removeCommentTag({id, asset_id, name})); + } }; module.exports = RootMutation; diff --git a/graph/resolvers/tag.js b/graph/resolvers/tag.js index e69de29bb..e1cd7922d 100644 --- a/graph/resolvers/tag.js +++ b/graph/resolvers/tag.js @@ -0,0 +1,5 @@ +const Tag = { + +}; + +module.exports = Tag; diff --git a/graph/resolvers/tag_link.js b/graph/resolvers/tag_link.js new file mode 100644 index 000000000..5c0d39587 --- /dev/null +++ b/graph/resolvers/tag_link.js @@ -0,0 +1,9 @@ +const TagLink = { + assigned_by({assigned_by}, _, {user, loaders: {Users}}) { + if (user && user.hasRole('ADMIN')) { + return Users.getByID.load(assigned_by); + } + } +}; + +module.exports = TagLink; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 781b3b328..bbe3e4d17 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -52,14 +52,30 @@ type User { status: USER_STATUS } +# Tag represents the underlying Tag that can be either stored in a global list +# or added uniquely to the entity. type Tag { - # the actual tag for the comment. - id: String! - # the user that assigned the tag. If NULL then the system automatically tagged it. - added_by: String + # The actual name of the tag entry. + name: String! - # the time when the tag was assigned. + # The time that this Tag was created. + created_at: Date! +} + +# TagLink is used to associate a given Tag with a Model via a TagLink. +type TagLink { + + # The underlying Tag that is either duplicated from the global list or created + # uniquely for this specific model. + tag: Tag! + + # The user that assigned the tag. This TagLink could have been created by the + # system, in which case this will be null. It could also be null if the + # current user is not an Admin/Moderator. + assigned_by: User + + # The date that the TagLink was created. created_at: Date! } @@ -176,7 +192,7 @@ type Comment { body: String! # the tags on the comment - tags: [Tag] + tags: [TagLink!] # the user who authored the comment. user: User @@ -702,15 +718,15 @@ type SetCommentStatusResponse implements Response { # Response to addCommentTag mutation type AddCommentTagResponse implements Response { + # An array of errors relating to the mutation that occured. - comment: Comment errors: [UserError] } # Response to removeCommentTag mutation type RemoveCommentTagResponse implements Response { + # An array of errors relating to the mutation that occured. - comment: Comment errors: [UserError] } @@ -751,10 +767,10 @@ type RootMutation { setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse # Add tag to comment. - addCommentTag(id: ID!, tag: String!): AddCommentTagResponse + addCommentTag(id: ID!, asset_id: ID!, name: String!): AddCommentTagResponse # Remove tag from comment. - removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse + removeCommentTag(id: ID!, asset_id: ID!, name: String!): RemoveCommentTagResponse # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse diff --git a/models/action.js b/models/action.js index 36d83574e..8aa7322e3 100644 --- a/models/action.js +++ b/models/action.js @@ -1,17 +1,8 @@ const mongoose = require('../services/mongoose'); const uuid = require('uuid'); const Schema = mongoose.Schema; - -const ACTION_TYPES = [ - 'LIKE', - 'FLAG' -]; - -const ITEM_TYPES = [ - 'ASSETS', - 'COMMENTS', - 'USERS' -]; +const ACTION_TYPES = require('./enum/action_types'); +const ITEM_TYPES = require('./enum/item_types'); const ActionSchema = new Schema({ id: { diff --git a/models/comment.js b/models/comment.js index 219ec055d..e3443376b 100644 --- a/models/comment.js +++ b/models/comment.js @@ -1,13 +1,8 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; +const TagLinkSchema = require('./schema/tag_link'); const uuid = require('uuid'); - -const STATUSES = [ - 'ACCEPTED', - 'REJECTED', - 'PREMOD', - 'NONE' -]; +const COMMENT_STATUS = require('./enum/comment_status'); /** * The Mongo schema for a Comment Status. @@ -16,7 +11,7 @@ const STATUSES = [ const StatusSchema = new Schema({ type: { type: String, - enum: STATUSES, + enum: COMMENT_STATUS, }, // The User ID of the user that assigned the status. @@ -30,29 +25,6 @@ const StatusSchema = new Schema({ _id: false }); -/** - * The Mongo schema for a Comment Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - - // This is the actual 'tag' and we only permit tags that are in Setting.tags. - id: String, - - // The User ID of the user that added the tag. - added_by: { - type: String, - default: null - }, - - created_at: { - type: Date, - default: Date - } -}, { - _id: false -}); - /** * The Mongo schema for a Comment. * @type {Schema} @@ -73,12 +45,11 @@ const CommentSchema = new Schema({ status_history: [StatusSchema], status: { type: String, - enum: STATUSES, + enum: COMMENT_STATUS, default: 'NONE' }, parent_id: String, - - tags: [TagSchema], + tags: [TagLinkSchema], // Additional metadata stored on the field. metadata: { @@ -92,10 +63,9 @@ const CommentSchema = new Schema({ } }); -// Add the indexes on the comment tag. +// Add the indexes for the id of the comment. CommentSchema.index({ - 'id': 1, - 'tags.id': 1 + 'id': 1 }, { unique: true, background: false diff --git a/models/enum/action_types.js b/models/enum/action_types.js new file mode 100644 index 000000000..3a27d3b85 --- /dev/null +++ b/models/enum/action_types.js @@ -0,0 +1,4 @@ +module.exports = [ + 'LIKE', + 'FLAG' +]; diff --git a/models/enum/comment_status.js b/models/enum/comment_status.js new file mode 100644 index 000000000..64633e9de --- /dev/null +++ b/models/enum/comment_status.js @@ -0,0 +1,6 @@ +module.exports = [ + 'ACCEPTED', + 'REJECTED', + 'PREMOD', + 'NONE' +]; diff --git a/models/enum/item_types.js b/models/enum/item_types.js new file mode 100644 index 000000000..e95aff82d --- /dev/null +++ b/models/enum/item_types.js @@ -0,0 +1,5 @@ +module.exports = [ + 'ASSETS', + 'COMMENTS', + 'USERS' +]; diff --git a/models/enum/moderation_options.js b/models/enum/moderation_options.js new file mode 100644 index 000000000..0331d59e0 --- /dev/null +++ b/models/enum/moderation_options.js @@ -0,0 +1,4 @@ +module.exports = [ + 'PRE', + 'POST' +]; diff --git a/models/enum/user_roles.js b/models/enum/user_roles.js new file mode 100644 index 000000000..0711adcd7 --- /dev/null +++ b/models/enum/user_roles.js @@ -0,0 +1,4 @@ +module.exports = [ + 'ADMIN', + 'MODERATOR' +]; diff --git a/models/enum/user_status.js b/models/enum/user_status.js new file mode 100644 index 000000000..cec930904 --- /dev/null +++ b/models/enum/user_status.js @@ -0,0 +1,6 @@ +module.exports = [ + 'ACTIVE', + 'BANNED', + 'PENDING', + 'APPROVED' // Indicates that the users' username has been approved +]; diff --git a/models/schema/tag.js b/models/schema/tag.js new file mode 100644 index 000000000..2d97185a0 --- /dev/null +++ b/models/schema/tag.js @@ -0,0 +1,53 @@ +const mongoose = require('../../services/mongoose'); +const Schema = mongoose.Schema; + +const ITEM_TYPES = require('../enum/item_types'); +const USER_ROLES = require('../enum/user_roles'); + +/** + * The Mongo schema for a Tag. + * @type {Schema} + */ +const TagSchema = new Schema({ + + // The actual name of the tag. + name: String, + + // Contains permission data. + permissions: { + + // Determines if this tag is public or not. + public: { + type: Boolean, + default: true + }, + + // Determines if the owner of the Model can add/remove this tag on their own + // resources. + self: Boolean, + + // Determines other roles that are allowed to set this tag on other + // resources. + roles: [{ + type: String, + enum: USER_ROLES, + default: [] + }] + }, + + // A list of all the model types that this tag can be added to. + models: [{ + type: String, + enum: ITEM_TYPES + }], + + // The date for when the tag was created. + created_at: { + type: Date, + default: Date + } +}, { + _id: false +}); + +module.exports = TagSchema; diff --git a/models/schema/tag_link.js b/models/schema/tag_link.js new file mode 100644 index 000000000..1cc1dbd08 --- /dev/null +++ b/models/schema/tag_link.js @@ -0,0 +1,31 @@ +const mongoose = require('../../services/mongoose'); +const Schema = mongoose.Schema; +const TagSchema = require('./tag'); + +/** + * The Mongo schema for linking a Tag to a Model. + * @type {Schema} + */ +const TagLinkSchema = new Schema({ + + // A deep copy of the tag that is the origin for this link. If the ID matches + // with existing tags in the global/asset context then content will be + // substituted. + tag: TagSchema, + + // The User ID of the user that assigned the status. + assigned_by: { + type: String, + default: null + }, + + // The date when the tag was added to the model. + created_at: { + type: Date, + default: Date + } +}, { + _id: false +}); + +module.exports = TagLinkSchema; diff --git a/models/setting.js b/models/setting.js index 44f5ad5ca..aee266054 100644 --- a/models/setting.js +++ b/models/setting.js @@ -1,45 +1,7 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; - -const PERMISSIONS = [ - 'ADMIN' -]; - -/** - * The Mongo schema for a Tag. - * @type {Schema} - */ -const TagSchema = new Schema({ - id: { - type: String, - unique: true, - default: 'STAFF' - }, - public: Boolean, - text: { - type: Schema.Types.Mixed, - default: null - }, - permissions: [{ - type: String, - enum: PERMISSIONS, - default: 'ADMIN' - }], - models: [String], - - // Additional metadata stored on the field. - metadata: Schema.Types.Mixed -}, { - timestamps: { - createdAt: 'created_at', - updatedAt: 'updated_at' - } -}); - -const MODERATION_OPTIONS = [ - 'PRE', - 'POST' -]; +const TagSchema = require('./schema/tag'); +const MODERATION_OPTIONS = require('./enum/moderation_options'); /** * SettingSchema manages application settings that get used on front and backend. @@ -156,4 +118,3 @@ SettingSchema.method('merge', function(src) { const Setting = mongoose.model('Setting', SettingSchema); module.exports = Setting; -module.exports.MODERATION_OPTIONS = MODERATION_OPTIONS; diff --git a/models/user.js b/models/user.js index 688c37c2c..b61ddd6eb 100644 --- a/models/user.js +++ b/models/user.js @@ -4,41 +4,10 @@ const Schema = mongoose.Schema; const uuid = require('uuid'); // USER_ROLES is the array of roles that is permissible as a user role. -const USER_ROLES = [ - 'ADMIN', - 'MODERATOR' -]; +const USER_ROLES = require('./enum/user_roles'); // USER_STATUS is the list of statuses that are permitted for the user status. -const USER_STATUS = [ - 'ACTIVE', - 'BANNED', - 'PENDING', - 'APPROVED' // Indicates that the users' username has been approved -]; - -// /** -// * The Mongo schema for a User Tag. -// * @type {Schema} -// */ -// const TagSchema = new Schema({ -// -// // This is the actual 'tag' and we only permit tags that are in Setting.tags. -// id: String, -// -// // The User ID of the user that added the tag. -// added_by: { -// type: String, -// default: null -// }, -// -// created_at: { -// type: Date, -// default: Date -// } -// }, { -// _id: false -// }); +const USER_STATUS = require('./enum/user_status'); // ProfileSchema is the mongoose schema defined as the representation of a // User's profile stored in MongoDB. @@ -245,12 +214,6 @@ UserSchema.method('can', function(...actions) { return false; } - // {add,remove}CommentTag - requires admin and/or moderator role - const userCanModifyTags = user => ['ADMIN', 'MODERATOR'].some(r => user.hasRoles(r)); - if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag', 'mutation:removeUserTag', 'mutation: addUserTag'].includes(a)) && ! userCanModifyTags(this)) { - return false; - } - return true; }); @@ -258,5 +221,3 @@ UserSchema.method('can', function(...actions) { const UserModel = mongoose.model('User', UserSchema); module.exports = UserModel; -module.exports.USER_ROLES = USER_ROLES; -module.exports.USER_STATUS = USER_STATUS; diff --git a/services/comments.js b/services/comments.js index e1873ba02..0ef15c552 100644 --- a/services/comments.js +++ b/services/comments.js @@ -1,23 +1,7 @@ const CommentModel = require('../models/comment'); - const ActionModel = require('../models/action'); const ActionsService = require('./actions'); -const SettingModel = require('../models/setting'); -const SettingsService = require('./settings'); -const UsersService = require('./users'); - -const errors = require('../errors'); - -const STATUSES = [ - 'ACCEPTED', - 'REJECTED', - 'PREMOD', - 'NONE', -]; - -const ALLOWED_TAGS = [ - 'STAFF' -]; +const COMMENT_STATUS = require('../models/enum/comment_status'); module.exports = class CommentsService { @@ -47,82 +31,52 @@ module.exports = class CommentsService { return commentModel.save(); } - /** - * Adds a tag if it doesn't already exist on the comment. - * @throws if tag is already added to the comment - * @throws if tag name is not in ALLOWED_TAGS - * @param {String} id the id of the comment to tag - * @param {String} name the name of the tag to add - * @param {String} added_by the user id for the user who added the tag - */ - static addTag(id, name, added_by) { + static addTag(id, tagLink, verifyOwnership = false) { - // Check that the tag is allowed by the system OR in our setting.tags. - return SettingsService.retrieve() - .then((settings) => { + // Compose the query to find the comment. + const query = { + id, + 'tags.tag.name': { + $ne: tagLink.tag.name + } + }; - UsersService.findById(added_by) - .then((user) => { + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (verifyOwnership) { + query['author_id'] = tagLink.assigned_by; + } - // Moderators or ADMIN can add any tag automatically. - if (user != null && (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { - SettingModel.findOneAndUpdate({id: settings.id}, { - $push: { - tags: { - id: name, - models: ['COMMENTS'] - } - } - }); - } - else if (!ALLOWED_TAGS.includes(name) || settings.tags.findIndex((t) => {return t.id === name & t.models.include('COMMENTS');}) === -1) { - return Promise.reject(errors.ErrTagNotAllowed); - } - }); - - return CommentModel.findOneAndUpdate({id, 'tags.id': {$ne: name}}, { - $push: { - tags: { - id: name, - added_by: added_by - } - }, - }) - .then(({nModified}) => { - switch (nModified) { - case 0: - return Promise.reject(errors.ErrNoCommentFound); - case 1: - return; - default: - } - }); + return CommentModel.update(query, { + $push: { + tags: tagLink + } }); } - /** - * Removes a tag from a comment - * @throws if the tag is not on the comment - * @param {String} id the id of the comment to tag - * @param {String} tag_id the id of the tag to remove - */ - static removeTag(id, tag_id) { - return CommentModel.findOneAndUpdate({id, 'tags.id': tag_id}, { + static removeTag(id, {tag: {name}, assigned_by}, verifyOwnership = false) { + + // Compose the query to find the comment that has the id: `id` and a tag + // that has the name: `name`. + const query = { + id, + 'tags.tag.name': { + $eq: name + } + }; + + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (verifyOwnership) { + query['author_id'] = assigned_by; + } + + return CommentModel.update(query, { $pull: { tags: { - id: tag_id + name } } - } - ) - .then(({nModified}) => { - switch(nModified) { - case 0: - return Promise.reject(errors.ErrNoCommentFound); - case 1: - return; - default: - } }); } @@ -226,7 +180,7 @@ module.exports = class CommentsService { static pushStatus(id, status, assigned_by = null) { // Check to see if the comment status is in the allowable set of statuses. - if (STATUSES.indexOf(status) === -1) { + if (COMMENT_STATUS.indexOf(status) === -1) { // Comment status is not supported! Error out here. return Promise.reject(new Error(`status ${status} is not supported`)); diff --git a/services/tags.js b/services/tags.js new file mode 100644 index 000000000..94212cb13 --- /dev/null +++ b/services/tags.js @@ -0,0 +1,5 @@ +class TagsService { + +} + +module.exports = TagsService; diff --git a/services/users.js b/services/users.js index 14301ccf4..53b51b686 100644 --- a/services/users.js +++ b/services/users.js @@ -12,8 +12,8 @@ const redis = require('./redis'); const redisClient = redis.createClient(); const UserModel = require('../models/user'); -const USER_STATUS = require('../models/user').USER_STATUS; -const USER_ROLES = require('../models/user').USER_ROLES; +const USER_STATUS = require('../models/enum/user_status'); +const USER_ROLES = require('../models/enum/user_roles'); const RECAPTCHA_WINDOW_SECONDS = 60 * 10; // 10 minutes. const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha will be required. diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 83a12dc2e..5783a14c1 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -3,23 +3,25 @@ const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); const Context = require('../../../../graph/context'); +const AssetModel = require('../../../../models/asset'); const UserModel = require('../../../../models/user'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); describe('graph.mutations.addCommentTag', () => { - let comment; + let comment, asset; beforeEach(async () => { await SettingsService.init(); - comment = await CommentsService.publicCreate({body: `hello there! ${ String(Math.random()).slice(2)}`}); + + asset = new AssetModel({url: 'http://new.test.com/'}); + await asset.save(); + + comment = await CommentsService.publicCreate({asset_id: asset.id, body: `hello there! ${String(Math.random()).slice(2)}`}); }); const query = ` - mutation AddCommentTag ($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { - comment { - id - } + mutation AddCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { + addCommentTag(id: $id, asset_id: $asset_id, name: $name) { errors { translation_key } @@ -30,16 +32,14 @@ describe('graph.mutations.addCommentTag', () => { it('moderators can add tags to comments', async () => { const user = new UserModel({roles: ['MODERATOR' ]}); const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); } expect(response.errors).to.be.empty; - return CommentsService.findById(response.data.addCommentTag.comment.id) - .then(({tags}) => { - expect(tags).to.have.length(1); - }); + let {tags} = await CommentsService.findById(comment.id); + expect(tags).to.have.length(1); }); describe('users who cant add tags', () => { @@ -48,15 +48,14 @@ describe('graph.mutations.addCommentTag', () => { 'regular commenter': new UserModel({}), 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) }).forEach(([ userDescription, user ]) => { - it(userDescription, async function () { + it(userDescription, async () => { const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST', privacy_type: 'PUBLIC'}); + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); } expect(response.errors).to.be.empty; expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); - expect(response.data.addCommentTag.comment).to.be.null; }); }); }); diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index 41822fcdb..e32943720 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -21,7 +21,9 @@ describe('graph.mutations.createComment', () => { id status tags { - id + tag { + name + } } } errors { @@ -228,7 +230,7 @@ describe('graph.mutations.createComment', () => { .then(({tags}) => { if (tag) { expect(tags).to.have.length(1); - expect(tags[0]).to.have.property('id', tag); + expect(tags[0].tag.name).to.have.equal(tag); } else { expect(tags).length(0); } diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js index 760887a44..cca072af2 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeCommentTag.js @@ -6,22 +6,24 @@ const Context = require('../../../../graph/context'); const UserModel = require('../../../../models/user'); const SettingModel = require('../../../../models/setting'); +const AssetModel = require('../../../../models/asset'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); describe('graph.mutations.removeCommentTag', () => { - let comment; + let asset, comment; beforeEach(async () => { await SettingsService.init(); - comment = await CommentsService.publicCreate({body: `hello there! ${ String(Math.random()).slice(2)}`}); + + asset = new AssetModel({url: 'http://new.test.com/'}); + await asset.save(); + + comment = await CommentsService.publicCreate({asset_id: asset.id, body: `hello there! ${String(Math.random()).slice(2)}`}); }); const query = ` - mutation RemoveCommentTag ($id: ID!, $tag: String!) { - removeCommentTag(id:$id, tag:$tag) { - comment { - id - } + mutation RemoveCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { + removeCommentTag(id: $id, asset_id: $asset_id, name: $name) { errors { translation_key } @@ -30,57 +32,53 @@ describe('graph.mutations.removeCommentTag', () => { `; it('moderators can add remove tags from comments', async () => { - const user = new UserModel({roles: ['MODERATOR' ]}); + const user = new UserModel({roles: ['MODERATOR']}); const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user.id); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); + await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); + + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { console.error(response.errors); } expect(response.errors).to.be.empty; expect(response.data.removeCommentTag.errors).to.be.null; - CommentsService.findById(response.data.removeCommentTag.comment.id) - .then(({tags}) => { - expect(tags).to.deep.equal([]); - }); + let retrievedComment = await CommentsService.findById(comment.id); + expect(retrievedComment.tags).to.have.length(0); }); describe('users who cant remove tags', () => { - // allow the tag in the settings - SettingModel.findOneAndUpdate({id: 1}, { + before(() => SettingModel.findOneAndUpdate({id: 1}, { $push: { tags: { id: 'BEST', models: ['COMMENTS'] } } - }) - .then(() => { - Object.entries({ - 'anonymous': undefined, - 'regular commenter': new UserModel({}), - 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) - }).forEach(([ userDescription, user ]) => { - it(userDescription, async function () { - const context = new Context({user}); + })); - // add a tag first - await CommentsService.addTag(comment.id, 'BEST', user.id); + Object.entries({ + 'anonymous': undefined, + 'regular commenter': new UserModel({}), + 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) + }).forEach(([userDescription, user]) => { + it(userDescription, async function () { + const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, tag: 'BEST'}); - if (response.errors && response.errors.length) { - console.error(response.errors); - } - expect(response.errors).to.be.empty; + // add a tag first + await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); - expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); - expect(response.data.removeCommentTag.comment).to.be.null; - }); + const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); + if (response.errors && response.errors.length) { + console.error(response.errors); + } + expect(response.errors).to.be.empty; + + expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); }); }); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 27657efa5..f5d82d1de 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -221,63 +221,84 @@ describe('services.CommentsService', () => { describe('#addTag', () => { it('adds a tag', async () => { - const commentId = comments[0].id; - const tagName = 'BEST'; - const userId = users[0].id; - await CommentsService.addTag(commentId, tagName, userId, 'PUBLIC'); - const {tags} = await CommentsService.findById(commentId); - expect(tags.length).to.equal(1); - expect(tags[0].id).to.equal(tagName); - expect(tags[0].added_by).to.equal(userId); - expect(tags[0].created_at).to.be.an.instanceof(Date); - }); - it('can\'t add a tag to comment id that doesn\'t exist', async () => { - const commentId = 'fakenews'; - const tagName = 'BEST'; - const userId = users[0].id; - - CommentsService.addTag(commentId, tagName, userId) - .catch((error) => { - expect(error).to.not.be.null; + const id = comments[0].id; + const name = 'BEST'; + const assigned_by = users[0].id; + + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by }); + + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + expect(tags[0].tag.name).to.equal(name); + expect(tags[0].assigned_by).to.equal(assigned_by); }); + it('can\'t add same tag.id twice', async () => { - const commentId = comments[0].id; - const tagName = 'BEST'; - const userId = users[0].id; + const id = comments[0].id; + const name = 'BEST'; + const assigned_by = users[0].id; + + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by + }); - // first time - await CommentsService.addTag(commentId, tagName, userId); + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } - // second time should fail - await expect(CommentsService.addTag(commentId, tagName, userId)).to.be.rejected; + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by + }); + + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } }); }); describe('#removeTag', () => { it('removes a tag', async () => { - const commentId = comments[0].id; - const tagName = 'BEST'; - await CommentsService.addTag(commentId, tagName, users[0].id); - const {tags} = await CommentsService.findById(commentId); - expect(tags.length).to.equal(1); + const id = comments[0].id; + const name = 'BEST'; + const assigned_by = users[0].id; + + await CommentsService.addTag(id, { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } // ok now to remove it - await CommentsService.removeTag(commentId, tagName); - const comment = await CommentsService.findById(commentId); - expect(comment.tags.length).to.equal(0); - }); - it('throws if removing a tag that isn\'t there', async () => { - const commentId = comments[0].id; + await CommentsService.removeTag(id, { + tag: { + name + }, + assigned_by + }); - const tagName = 'BEST'; - - // just make sure it has no tags to start - const {tags} = await CommentsService.findById(commentId); - expect(tags.length).to.equal(0); - - // ok now to remove it - await expect(CommentsService.removeTag(commentId, tagName)).to.be.rejected; + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(0); + } }); }); From 0d47ee895c4262fa15709578613f9797ae6d4845 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 15:02:31 -0600 Subject: [PATCH 018/264] removed unused service --- services/tags.js | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 services/tags.js diff --git a/services/tags.js b/services/tags.js deleted file mode 100644 index 94212cb13..000000000 --- a/services/tags.js +++ /dev/null @@ -1,5 +0,0 @@ -class TagsService { - -} - -module.exports = TagsService; From aeaf809a0f5c9ef618b4781549d840b82490588b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 11 May 2017 14:42:06 -0600 Subject: [PATCH 019/264] Made queries more generic related to tags --- graph/mutators/comment.js | 148 +----------------- graph/mutators/index.js | 2 + graph/mutators/tag.js | 82 ++++++++++ graph/resolvers/asset.js | 5 + graph/resolvers/comment.js | 5 + graph/resolvers/root_mutation.js | 8 +- graph/resolvers/user.js | 5 + graph/resolvers/util.js | 16 ++ graph/typeDefs.graphql | 112 ++++++++----- models/asset.js | 4 + models/comment.js | 2 + models/user.js | 10 +- .../server/typeDefs.graphql | 2 +- services/tags.js | 133 ++++++++++++++++ .../mutations/{addCommentTag.js => addTag.js} | 24 +-- .../{removeCommentTag.js => removeTag.js} | 10 +- 16 files changed, 356 insertions(+), 212 deletions(-) create mode 100644 graph/mutators/tag.js create mode 100644 graph/resolvers/util.js create mode 100644 services/tags.js rename test/server/graph/mutations/{addCommentTag.js => addTag.js} (65%) rename test/server/graph/mutations/{removeCommentTag.js => removeTag.js} (87%) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 3a57f3b3a..23c906f15 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -210,149 +210,11 @@ const setCommentStatus = ({user, loaders: {Comments}}, {id, status}) => { }); }; -/** - * Adds a tag to a Comment. - */ -const addCommentTag = async ({user, loaders: {Assets, Comments, Settings}}, {id, asset_id, name}) => { - - // If the current user does not have the ADMIN or MODERATOR role - if (!user || !user.can('mutation:addCommentTag')) { - throw errors.ErrNotAuthorized; - } - - // Load the settings from the dataloader, it will be cached if it was - // retrieved already. - let settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); - - // Try to find the tag in the asset tags. This will contain the permission - // information. - let tag = settings.tags.find((globalTag) => { - return globalTag.name === name && globalTag.models.include('COMMENTS'); - }); - - // Create the new tagLink that will be created to attach to the comment. - let tagLink = { - tag, - assigned_by: user.id, - created_at: new Date() - }; - - // If the tag was found, we need to ensure that the current user can indeed - // set this tag on the comment. - if (tag) { - - // If the tag has roles defined, and the current user has at least one of - // the required roles, then add the tag without checking for ownership. - if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { - return CommentsService.addTag(id, tagLink, false); - } - - // If the permissions allow for self assignment, then ensure that the query - // is compose with that in mind. - if (tag.permissions && tag.permissions.self) { - - // Otherwise, we assume that we have to check to see that the user indeed - // owns the resource before allowing the tag to get added. - return CommentsService.addTag(id, tagLink, true); - } - - throw errors.ErrNotAuthorized; - } - - // Only admin/moderators can add unique tags, these are tags that are not in - // the global list. - if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { - throw errors.ErrNotAuthorized; - } - - // Generate the tag in the event now that we have to create the tag for this - // specific comment. - tagLink.tag = { - name, - permissions: { - public: true, - self: false, - roles: [] - }, - models: ['COMMENTS'], - created_at: new Date() - }; - - // Actually attach the new tag that we created to the comment. - return CommentsService.addTag(id, tagLink, false); -}; - -/** - * Removes a tag from a Comment. - */ -const removeCommentTag = async ({user, loaders: {Comments}}, {id, asset_id, name}) => { - - // If the current user does not have the ADMIN or MODERATOR role - if (!user || !user.can('mutation:removeCommentTag')) { - throw errors.ErrNotAuthorized; - } - - // Load the settings from the dataloader, it will be cached if it was - // retrieved already. - let settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); - - // Try to find the tag in the asset tags. This will contain the permission - // information. - let tag = settings.tags.find((globalTag) => { - return globalTag.name === name && globalTag.models.include('COMMENTS'); - }); - - // Create the new tagLink that will be created to attach to the comment. - let tagLink = { - tag, - assigned_by: user.id - }; - - // If the tag was found, we need to ensure that the current user can indeed - // remove this tag on the comment. - if (tag) { - - // If the tag has roles defined, and the current user has at least one of - // the required roles, then remove the tag without checking for ownership. - if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { - return CommentsService.removeTag(id, tagLink, false); - } - - // If the permissions allow for self assignment, then ensure that the query - // is compose with that in mind. - if (tag.permissions && tag.permissions.self) { - - // Otherwise, we assume that we have to check to see that the user indeed - // owns the resource before allowing the tag to get removed. - return CommentsService.removeTag(id, tagLink, true); - } - - throw errors.ErrNotAuthorized; - } - - // Only admin/moderators can remove unique tags, these are tags that are not - // in the global list. - if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { - throw errors.ErrNotAuthorized; - } - - // Generate the tag in the event now that we have to create the tag for this - // specific comment. - tagLink.tag = { - name - }; - - // Actually attach the new tag that we created to the comment. - return CommentsService.removeTag(id, tagLink, false); -}; - module.exports = (context) => { let mutators = { Comment: { create: () => Promise.reject(errors.ErrNotAuthorized), - setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized), - addCommentTag: () => Promise.reject(errors.ErrNotAuthorized), - removeCommentTag: () => Promise.reject(errors.ErrNotAuthorized), + setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized) } }; @@ -364,13 +226,5 @@ module.exports = (context) => { mutators.Comment.setCommentStatus = (action) => setCommentStatus(context, action); } - if (context.user && context.user.can('mutation:addCommentTag')) { - mutators.Comment.addCommentTag = (action) => addCommentTag(context, action); - } - - if (context.user && context.user.can('mutation:removeCommentTag')) { - mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action); - } - return mutators; }; diff --git a/graph/mutators/index.js b/graph/mutators/index.js index 9975bedde..0076a5e8f 100644 --- a/graph/mutators/index.js +++ b/graph/mutators/index.js @@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:mutators'); const Comment = require('./comment'); const Action = require('./action'); +const Tag = require('./tag'); const User = require('./user'); const plugins = require('../../services/plugins'); @@ -12,6 +13,7 @@ let mutators = [ // Load in the core mutators. Comment, Action, + Tag, User, // Load the plugin mutators from the manager. diff --git a/graph/mutators/tag.js b/graph/mutators/tag.js new file mode 100644 index 000000000..3182a7d00 --- /dev/null +++ b/graph/mutators/tag.js @@ -0,0 +1,82 @@ +const TagsService = require('../../services/tags'); +const errors = require('../../errors'); + +/** + * Modifies the targeted model with the specified operation to add/remove a tag. + */ +const modify = async ({user}, operation, {name, id, item_type, asset_id}) => { + + // Try to find the tag in the global list. This will contain the permission + // information if it's found. + let tag = await TagsService.get({name, id, item_type, asset_id}); + + // Create the new tagLink that will be created to interact to the comment. + let tagLink = { + tag, + assigned_by: user.id, + created_at: new Date() + }; + + // If the tag was found, we need to ensure that the current user can indeed + // modify this tag on the comment. + if (tag) { + + // If the tag has roles defined, and the current user has at least one of + // the required roles, then modify the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return operation(id, item_type, tagLink, false); + } + + // If the permissions allow for self assignment, then ensure that the query + // is compose with that in mind. + if (tag.permissions && tag.permissions.self) { + + // Otherwise, we assume that we have to check to see that the user indeed + // owns the resource before allowing the tag to get modified. + return operation(id, item_type, tagLink, true); + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can modify unique tags, these are tags that are not + // in the global list. + if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { + throw errors.ErrNotAuthorized; + } + + // Generate the tag in the event now that we have to create the tag for this + // specific comment. + tagLink.tag = { + name, + permissions: { + public: true, + self: false, + roles: [] + }, + models: [item_type], + created_at: new Date() + }; + + // Actually modify the tag on the model. + return operation(id, item_type, tagLink, false); +}; + +module.exports = (context) => { + let mutators = { + Tag: { + add: () => Promise.reject(errors.ErrNotAuthorized), + remove: () => Promise.reject(errors.ErrNotAuthorized) + } + }; + + if (context.user && context.user.can('mutation:addTag')) { + mutators.Tag.add = (tag) => modify(context, TagsService.add, tag); + } + + if (context.user && context.user.can('mutation:removeTag')) { + mutators.Tag.remove = (tag) => modify(context, TagsService.remove, tag); + } + + return mutators; +}; diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index c260d0070..012e77927 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -1,3 +1,5 @@ +const {decorateWithTags} = require('./util'); + const Asset = { lastComment({id}, _, {loaders: {Comments}}) { return Comments.getByQuery({ @@ -49,4 +51,7 @@ const Asset = { } }; +// Decorate the Asset type resolver with a tags field. +decorateWithTags(Asset); + module.exports = Asset; diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 88ddbee07..b0251f626 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -1,3 +1,5 @@ +const {decorateWithTags} = require('./util'); + const Comment = { parent({parent_id}, _, {loaders: {Comments}}) { if (parent_id == null) { @@ -48,4 +50,7 @@ const Comment = { } }; +// Decorate the Comment type resolver with a tags field. +decorateWithTags(Comment); + module.exports = Comment; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 442845bd2..3d18bb277 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -28,11 +28,11 @@ const RootMutation = { setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, - addCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.addCommentTag({id, asset_id, name})); + addTag(_, {tag}, {mutators: {Tag}}) { + return wrapResponse(null)(Tag.add(tag)); }, - removeCommentTag(_, {id, asset_id, name}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.removeCommentTag({id, asset_id, name})); + removeTag(_, {tag}, {mutators: {Tag}}) { + return wrapResponse(null)(Tag.remove(tag)); } }; diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index d8ed7ee15..53f4f7c1b 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -1,3 +1,5 @@ +const {decorateWithTags} = require('./util'); + const User = { action_summaries({id}, _, {loaders: {Actions}}) { return Actions.getSummariesByItemID.load(id); @@ -31,4 +33,7 @@ const User = { } }; +// Decorate the User type resolver with a tags field. +decorateWithTags(User); + module.exports = User; diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js new file mode 100644 index 000000000..11cfcbb13 --- /dev/null +++ b/graph/resolvers/util.js @@ -0,0 +1,16 @@ +/** + * Decorates the typeResolver with the tags field. + */ +const decorateWithTags = (typeResolver) => { + typeResolver.tags = ({tags = []}, _, {user}) => { + if (user && user.hasRoles('ADMIN')) { + return tags; + } + + return tags.filter((tag) => tag.public); + }; +}; + +module.exports = { + decorateWithTags +}; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index bbe3e4d17..b6d29e5ef 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -39,6 +39,9 @@ type User { # the current roles of the user. roles: [USER_ROLES] + # the tags on the user + tags: [TagLink!] + # determines whether the user can edit their username canEditName: Boolean @@ -52,6 +55,38 @@ type User { status: USER_STATUS } +# UsersQuery allows the ability to query users by a specific fields. +input UsersQuery { + action_type: ACTION_TYPE + + # Limit the number of results to be returned. + limit: Int = 10 + + # Skip results from the last created_at timestamp. + cursor: Date + + # Sort the results by created_at. + sort: SORT_ORDER = REVERSE_CHRONOLOGICAL +} + + +################################################################################ +## Tags +################################################################################ + +# Used to represent the item type for a tag. +enum TAGGABLE_ITEM_TYPE { + + # The action references a entity of type Asset. + ASSETS + + # The action references a entity of type Comment. + COMMENTS + + # The action references a entity of type User. + USERS +} + # Tag represents the underlying Tag that can be either stored in a global list # or added uniquely to the entity. type Tag { @@ -79,20 +114,6 @@ type TagLink { created_at: Date! } -# UsersQuery allows the ability to query users by a specific fields. -input UsersQuery { - action_type: ACTION_TYPE - - # Limit the number of results to be returned. - limit: Int = 10 - - # Skip results from the last created_at timestamp. - cursor: Date - - # Sort the results by created_at. - sort: SORT_ORDER = REVERSE_CHRONOLOGICAL -} - ################################################################################ ## Comments ################################################################################ @@ -459,6 +480,9 @@ type Asset { # The date that the asset was created. created_at: Date + # the tags on the asset + tags: [TagLink!] + # The author(s) of the asset. author: String } @@ -495,7 +519,7 @@ type ValidationUserError implements UserError { } ################################################################################ -## Queries; +## Queries ################################################################################ # Establishes the ordering of the content by their created_at time stamp. @@ -577,7 +601,7 @@ type RootQuery { interface Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # CreateCommentResponse is returned with the comment that was created and any @@ -588,7 +612,7 @@ type CreateCommentResponse implements Response { comment: Comment # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # Used to represent the item type for an action. @@ -653,7 +677,7 @@ type CreateFlagResponse implements Response { flag: FlagAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } @@ -666,7 +690,7 @@ type CreateDontAgreeResponse implements Response { dontagree: DontAgreeAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } input CreateDontAgreeInput { @@ -689,7 +713,7 @@ input CreateDontAgreeInput { type DeleteActionResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # SetUserStatusResponse is the response returned with possibly some errors @@ -697,7 +721,7 @@ type DeleteActionResponse implements Response { type SetUserStatusResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # SuspendUserResponse is the response returned with possibly some errors @@ -705,7 +729,7 @@ type SetUserStatusResponse implements Response { type SuspendUserResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } # SetCommentStatusResponse is the response returned with possibly some errors @@ -713,33 +737,43 @@ type SuspendUserResponse implements Response { type SetCommentStatusResponse implements Response { # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } -# Response to addCommentTag mutation -type AddCommentTagResponse implements Response { +# ModifyTagInput is the input used to modify a tag. +input ModifyTagInput { + + # name is the actual tag to add to the model. + name: String! + + # id is the ID of the model in question that we are modifying the tag of. + id: ID! + + # item_type is the type of item that we are modifying the tag if. + item_type: TAGGABLE_ITEM_TYPE! + + # asset_id is used when the item_type is `COMMENTS`, the is needed to rectify + # the settings to get the asset specific tags/settings. + asset_id: ID +} + +# Response to the addTag or removeTag mutations. +type ModifyTagResponse implements Response { # An array of errors relating to the mutation that occured. - errors: [UserError] -} - -# Response to removeCommentTag mutation -type RemoveCommentTagResponse implements Response { - - # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # Response to ignoreUser mutation type IgnoreUserResponse implements Response { # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # Response to stopIgnoringUser mutation type StopIgnoringUserResponse implements Response { # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # All mutations for the application are defined on this object. @@ -766,11 +800,11 @@ type RootMutation { # Sets Comment status. Requires the `ADMIN` role. setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse - # Add tag to comment. - addCommentTag(id: ID!, asset_id: ID!, name: String!): AddCommentTagResponse + # Add a tag. + addTag(tag: ModifyTagInput!): ModifyTagResponse! - # Remove tag from comment. - removeCommentTag(id: ID!, asset_id: ID!, name: String!): RemoveCommentTagResponse + # Removes a tag. + removeTag(tag: ModifyTagInput!): ModifyTagResponse! # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse diff --git a/models/asset.js b/models/asset.js index 51ff42705..1aeec34f3 100644 --- a/models/asset.js +++ b/models/asset.js @@ -1,6 +1,7 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; const uuid = require('uuid'); +const TagLinkSchema = require('./schema/tag_link'); const AssetSchema = new Schema({ id: { @@ -47,6 +48,9 @@ const AssetSchema = new Schema({ default: null }, + // Tags are added by the self or by administrators. + tags: [TagLinkSchema], + // Additional metadata stored on the field. metadata: { default: {}, diff --git a/models/comment.js b/models/comment.js index e3443376b..76065f5cb 100644 --- a/models/comment.js +++ b/models/comment.js @@ -49,6 +49,8 @@ const CommentSchema = new Schema({ default: 'NONE' }, parent_id: String, + + // Tags are added by the self or by administrators. tags: [TagLinkSchema], // Additional metadata stored on the field. diff --git a/models/user.js b/models/user.js index b61ddd6eb..d069ac457 100644 --- a/models/user.js +++ b/models/user.js @@ -2,6 +2,7 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); const Schema = mongoose.Schema; const uuid = require('uuid'); +const TagLinkSchema = require('./schema/tag_link'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = require('./enum/user_roles'); @@ -118,6 +119,9 @@ const UserSchema = new Schema({ type: String, }], + // Tags are added by the self or by administrators. + tags: [TagLinkSchema], + // Additional metadata stored on the field. metadata: { default: {}, @@ -191,10 +195,8 @@ const USER_GRAPH_OPERATIONS = [ 'mutation:setUserStatus', 'mutation:suspendUser', 'mutation:setCommentStatus', - 'mutation:addCommentTag', - 'mutation:removeCommentTag', - 'mutation:addUserTag', - 'mutation:removeUserTag' + 'mutation:addTag', + 'mutation:removeTag' ]; /** diff --git a/plugins/coral-plugin-respect/server/typeDefs.graphql b/plugins/coral-plugin-respect/server/typeDefs.graphql index 6639aa454..56734e543 100644 --- a/plugins/coral-plugin-respect/server/typeDefs.graphql +++ b/plugins/coral-plugin-respect/server/typeDefs.graphql @@ -44,7 +44,7 @@ type CreateRespectResponse implements Response { respect: RespectAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } type RootMutation { diff --git a/services/tags.js b/services/tags.js new file mode 100644 index 000000000..3ee3de121 --- /dev/null +++ b/services/tags.js @@ -0,0 +1,133 @@ +const CommentModel = require('../models/comment'); +const AssetModel = require('../models/asset'); +const UserModel = require('../models/user'); + +const AssetsService = require('./assets'); +const SettingsService = require('./settings'); + +const updateModel = async (item_type, query, update) => { + + // Get the model to update with. + let Model; + switch (item_type) { + case 'COMMENTS': + Model = CommentModel; + break; + case 'ASSETS': + Model = AssetModel; + break; + case 'USERS': + Model = UserModel; + break; + default: + throw new Error(`item_type ${item_type} is not a valid item_type to update a tag on`); + } + + // Execute the update operation. + return Model.update(query, update); +}; + +const ownershipQuery = async (item_type, link, query) => { + switch (item_type) { + case 'COMMENTS': + query['author_id'] = link.assigned_by; + break; + case 'USERS': + query['id'] = link.assigned_by; + break; + } +}; + +class TagsService { + + /** + * Retrives a global tag from the settings based on the input_type. + */ + static async get({name, id, item_type, asset_id = null}) { + + // Extract the settings from the database. + let settings; + switch (item_type) { + case 'COMMENTS': + settings = await AssetsService.rectifySettings(AssetsService.findById(asset_id)); + break; + case 'ASSETS': + settings = await AssetsService.rectifySettings(AssetsService.findById(id)); + break; + case 'USERS': + settings = await SettingsService.retrieve(); + break; + default: + settings = await SettingsService.retrieve(); + break; + } + + // Extract the tags from the settings object. + let {tags = []} = settings; + + // Return the first tag that matches the requested form. + return tags.find((tag) => tag.name === name && tag.models.include(item_type)); + } + + /** + * Adds a TagLink to a Model, optionally checking for ownership. + */ + static async add(id, item_type, link, ownershipCheck) { + + // Compose the query to find the comment. + const query = { + id, + 'tags.tag.name': { + $ne: link.tag.name + } + }; + + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (ownershipCheck) { + + // Modify the query to support an ownership verification. + ownershipQuery(item_type, link, query); + } + + // Get the Model to perform the update. + return updateModel(item_type, query, { + $push: { + tags: link + } + }); + } + + /** + * Removes a TagLink to a Model, optionally checking for ownership. + */ + static async remove(id, item_type, link, ownershipCheck) { + + // Compose the query to find the comment. + const query = { + id, + 'tags.tag.name': { + $eq: link.tag.name + } + }; + + // If ownership verification is required, ensure that the person that is + // assigning the tag is the same person that owns the comment. + if (ownershipCheck) { + + // Modify the query to support an ownership verification. + ownershipQuery(item_type, link, query); + } + + // Get the Model to perform the update. + return updateModel(item_type, query, { + $pull: { + tags: { + name: link.tag.name + } + } + }); + } +} + +module.exports = TagsService; diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addTag.js similarity index 65% rename from test/server/graph/mutations/addCommentTag.js rename to test/server/graph/mutations/addTag.js index 5783a14c1..e6639d49c 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addTag.js @@ -8,7 +8,7 @@ const UserModel = require('../../../../models/user'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); -describe('graph.mutations.addCommentTag', () => { +describe('graph.mutations.addTag', () => { let comment, asset; beforeEach(async () => { await SettingsService.init(); @@ -21,7 +21,7 @@ describe('graph.mutations.addCommentTag', () => { const query = ` mutation AddCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { - addCommentTag(id: $id, asset_id: $asset_id, name: $name) { + addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key } @@ -30,13 +30,13 @@ describe('graph.mutations.addCommentTag', () => { `; it('moderators can add tags to comments', async () => { - const user = new UserModel({roles: ['MODERATOR' ]}); + const user = new UserModel({roles: ['MODERATOR']}); const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); - if (response.errors && response.errors.length) { - console.error(response.errors); + const res = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}, 'AddCommentTag'); + if (res.errors && res.errors.length) { + console.error(res.errors); } - expect(response.errors).to.be.empty; + expect(res.errors).to.be.empty; let {tags} = await CommentsService.findById(comment.id); expect(tags).to.have.length(1); @@ -50,12 +50,12 @@ describe('graph.mutations.addCommentTag', () => { }).forEach(([ userDescription, user ]) => { it(userDescription, async () => { const context = new Context({user}); - const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); - if (response.errors && response.errors.length) { - console.error(response.errors); + const res = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}, 'AddCommentTag'); + if (res.errors && res.errors.length) { + console.error(res.errors); } - expect(response.errors).to.be.empty; - expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); + expect(res.errors).to.be.empty; + expect(res.data.addTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); }); }); }); diff --git a/test/server/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeTag.js similarity index 87% rename from test/server/graph/mutations/removeCommentTag.js rename to test/server/graph/mutations/removeTag.js index cca072af2..1075cab49 100644 --- a/test/server/graph/mutations/removeCommentTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -10,7 +10,7 @@ const AssetModel = require('../../../../models/asset'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); -describe('graph.mutations.removeCommentTag', () => { +describe('graph.mutations.removeTag', () => { let asset, comment; beforeEach(async () => { await SettingsService.init(); @@ -22,8 +22,8 @@ describe('graph.mutations.removeCommentTag', () => { }); const query = ` - mutation RemoveCommentTag ($id: ID!, $asset_id: ID!, $name: String!) { - removeCommentTag(id: $id, asset_id: $asset_id, name: $name) { + mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key } @@ -43,7 +43,7 @@ describe('graph.mutations.removeCommentTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; - expect(response.data.removeCommentTag.errors).to.be.null; + expect(response.data.removeTag.errors).to.be.null; let retrievedComment = await CommentsService.findById(comment.id); @@ -78,7 +78,7 @@ describe('graph.mutations.removeCommentTag', () => { } expect(response.errors).to.be.empty; - expect(response.data.removeCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); + expect(response.data.removeTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]); }); }); }); From 0d957bcfb9878620c2575e92fe5e5e15bc9e6810 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 11 May 2017 14:56:36 -0600 Subject: [PATCH 020/264] Code cleanup related to old tag services --- services/comments.js | 49 ----------- test/server/graph/mutations/removeTag.js | 5 +- test/server/services/comments.js | 83 ------------------ test/server/services/tags.js | 107 +++++++++++++++++++++++ 4 files changed, 110 insertions(+), 134 deletions(-) create mode 100644 test/server/services/tags.js diff --git a/services/comments.js b/services/comments.js index e23e4e486..ca258934f 100644 --- a/services/comments.js +++ b/services/comments.js @@ -31,55 +31,6 @@ module.exports = class CommentsService { return commentModel.save(); } - static addTag(id, tagLink, verifyOwnership = false) { - - // Compose the query to find the comment. - const query = { - id, - 'tags.tag.name': { - $ne: tagLink.tag.name - } - }; - - // If ownership verification is required, ensure that the person that is - // assigning the tag is the same person that owns the comment. - if (verifyOwnership) { - query['author_id'] = tagLink.assigned_by; - } - - return CommentModel.update(query, { - $push: { - tags: tagLink - } - }); - } - - static removeTag(id, {tag: {name}, assigned_by}, verifyOwnership = false) { - - // Compose the query to find the comment that has the id: `id` and a tag - // that has the name: `name`. - const query = { - id, - 'tags.tag.name': { - $eq: name - } - }; - - // If ownership verification is required, ensure that the person that is - // assigning the tag is the same person that owns the comment. - if (verifyOwnership) { - query['author_id'] = assigned_by; - } - - return CommentModel.update(query, { - $pull: { - tags: { - name - } - } - }); - } - /** * Finds a comment by the id. * @param {String} id identifier of comment (uuid) diff --git a/test/server/graph/mutations/removeTag.js b/test/server/graph/mutations/removeTag.js index 1075cab49..a7ee4053b 100644 --- a/test/server/graph/mutations/removeTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -9,6 +9,7 @@ const SettingModel = require('../../../../models/setting'); const AssetModel = require('../../../../models/asset'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); +const TagsService = require('../../../../services/tags'); describe('graph.mutations.removeTag', () => { let asset, comment; @@ -36,7 +37,7 @@ describe('graph.mutations.removeTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); + await TagsService.add(comment.id, 'COMMENTS', {tag: {name: 'BEST'}}, false); const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { @@ -70,7 +71,7 @@ describe('graph.mutations.removeTag', () => { const context = new Context({user}); // add a tag first - await CommentsService.addTag(comment.id, {tag: {name: 'BEST'}}, false); + await TagsService.add(comment.id, 'COMMENTS', {tag: {name: 'BEST'}}, false); const response = await graphql(schema, query, {}, context, {id: comment.id, asset_id: asset.id, name: 'BEST'}); if (response.errors && response.errors.length) { diff --git a/test/server/services/comments.js b/test/server/services/comments.js index f5d82d1de..68859077f 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -219,89 +219,6 @@ describe('services.CommentsService', () => { }); - describe('#addTag', () => { - it('adds a tag', async () => { - const id = comments[0].id; - const name = 'BEST'; - const assigned_by = users[0].id; - - await CommentsService.addTag(id, { - tag: { - name - }, - assigned_by - }); - - const {tags} = await CommentsService.findById(id); - expect(tags.length).to.equal(1); - expect(tags[0].tag.name).to.equal(name); - expect(tags[0].assigned_by).to.equal(assigned_by); - }); - - it('can\'t add same tag.id twice', async () => { - const id = comments[0].id; - const name = 'BEST'; - const assigned_by = users[0].id; - - await CommentsService.addTag(id, { - tag: { - name - }, - assigned_by - }); - - { - let {tags} = await CommentsService.findById(id); - expect(tags.length).to.equal(1); - } - - await CommentsService.addTag(id, { - tag: { - name - }, - assigned_by - }); - - { - let {tags} = await CommentsService.findById(id); - expect(tags.length).to.equal(1); - } - }); - }); - - describe('#removeTag', () => { - it('removes a tag', async () => { - const id = comments[0].id; - const name = 'BEST'; - const assigned_by = users[0].id; - - await CommentsService.addTag(id, { - tag: { - name - }, - assigned_by - }); - - { - const {tags} = await CommentsService.findById(id); - expect(tags.length).to.equal(1); - } - - // ok now to remove it - await CommentsService.removeTag(id, { - tag: { - name - }, - assigned_by - }); - - { - const {tags} = await CommentsService.findById(id); - expect(tags.length).to.equal(0); - } - }); - }); - describe('#changeStatus', () => { it('should change the status of a comment from no status', () => { diff --git a/test/server/services/tags.js b/test/server/services/tags.js new file mode 100644 index 000000000..7cdad829e --- /dev/null +++ b/test/server/services/tags.js @@ -0,0 +1,107 @@ +const CommentsService = require('../../../services/comments'); +const TagsService = require('../../../services/tags'); +const UsersService = require('../../../services/users'); +const SettingsService = require('../../../services/settings'); + +const CommentModel = require('../../../models/comment'); + +const expect = require('chai').use(require('chai-as-promised')).expect; + +describe('services.TagsService', () => { + let comment, user; + beforeEach(async () => { + await SettingsService.init(); + user = await UsersService.createLocalUser('stampi@gmail.com', '1Coral!!', 'Stampi'); + comment = await CommentModel.create({ + id: '1', + body: 'comment 10', + asset_id: '123', + status_history: [], + parent_id: null, + author_id: user.id + }); + }); + + describe('#add', () => { + it('adds a tag', async () => { + const id = comment.id; + const name = 'BEST'; + const assigned_by = user.id; + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + expect(tags[0].tag.name).to.equal(name); + expect(tags[0].assigned_by).to.equal(assigned_by); + }); + + it('can\'t add same tag.id twice', async () => { + const id = comment.id; + const name = 'BEST'; + const assigned_by = user.id; + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + let {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } + }); + }); + + describe('#remove', () => { + it('removes a tag', async () => { + const id = comment.id; + const name = 'BEST'; + const assigned_by = user.id; + + await TagsService.add(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(1); + } + + // ok now to remove it + await TagsService.remove(id, 'COMMENTS', { + tag: { + name + }, + assigned_by + }); + + { + const {tags} = await CommentsService.findById(id); + expect(tags.length).to.equal(0); + } + }); + }); +}); From e6b2ff19b8435cdc8e133cdf4e9ef1f81992dcc9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 12 May 2017 07:51:53 -0600 Subject: [PATCH 021/264] fixing graph merge error --- graph/typeDefs.graphql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index e69a39f23..a8d13e80c 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -104,12 +104,12 @@ type TagLink { # The underlying Tag that is either duplicated from the global list or created # uniquely for this specific model. tag: Tag! - + # The user that assigned the tag. This TagLink could have been created by the # system, in which case this will be null. It could also be null if the # current user is not an Admin/Moderator. assigned_by: User - + # The date that the TagLink was created. created_at: Date! } @@ -767,7 +767,7 @@ input ModifyTagInput { # Response to the addTag or removeTag mutations. type ModifyTagResponse implements Response { - + # An array of errors relating to the mutation that occured. errors: [UserError!] } @@ -798,7 +798,7 @@ type CommentInfoAfterEdit { type EditCommentResponse implements Response { comment: CommentInfoAfterEdit! # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError!] } # All mutations for the application are defined on this object. From b60ed166d0fd70f3aabae7b7babae7c93bac920e Mon Sep 17 00:00:00 2001 From: David Erwin Date: Fri, 12 May 2017 15:38:38 -0400 Subject: [PATCH 022/264] Allow MODERATORs to access tags --- graph/resolvers/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index 11cfcbb13..96e125a55 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -3,7 +3,7 @@ */ const decorateWithTags = (typeResolver) => { typeResolver.tags = ({tags = []}, _, {user}) => { - if (user && user.hasRoles('ADMIN')) { + if (user && (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { return tags; } From c3d02a14a0fc43cba9ae26f2d77a37441c960363 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 15 May 2017 18:11:14 -0300 Subject: [PATCH 023/264] Updated queries and hasRoles --- .../src/containers/Comment.js | 8 ++++++- .../coral-embed-stream/src/graphql/index.js | 24 ++++++++++++++++--- graph/resolvers/tag_link.js | 2 +- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 4a8e877b5..7b5159cc1 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -28,7 +28,13 @@ export default withFragments({ created_at status tags { - id + tag { + name + created_at + } + assigned_by { + id + } } user { id diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index daa8389b3..d43bfd3d5 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -33,7 +33,13 @@ const extension = { comment { id tags { - name + tag { + name + created_at + } + assigned_by { + id + } } } errors { @@ -46,7 +52,13 @@ const extension = { comment { id tags { - name + tag { + name + created_at + } + assigned_by { + id + } } } errors { @@ -101,7 +113,13 @@ const extension = { status replyCount tags { - name + tag { + name + created_at + } + assigned_by { + id + } } user { id diff --git a/graph/resolvers/tag_link.js b/graph/resolvers/tag_link.js index 5c0d39587..11ee1350d 100644 --- a/graph/resolvers/tag_link.js +++ b/graph/resolvers/tag_link.js @@ -1,6 +1,6 @@ const TagLink = { assigned_by({assigned_by}, _, {user, loaders: {Users}}) { - if (user && user.hasRole('ADMIN')) { + if (user && user.hasRoles('ADMIN')) { return Users.getByID.load(assigned_by); } } From c59346eb0ae5b347669bb73de89f1c03b7ba8b65 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 15 May 2017 18:13:45 -0300 Subject: [PATCH 024/264] tags, and assignedby nul --- graph/resolvers/tag_link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/resolvers/tag_link.js b/graph/resolvers/tag_link.js index 11ee1350d..2fede45ec 100644 --- a/graph/resolvers/tag_link.js +++ b/graph/resolvers/tag_link.js @@ -1,6 +1,6 @@ const TagLink = { assigned_by({assigned_by}, _, {user, loaders: {Users}}) { - if (user && user.hasRoles('ADMIN')) { + if (user && user.hasRoles('ADMIN') && assigned_by != null) { return Users.getByID.load(assigned_by); } } From 793f63495915e47bc0678f6feda738d21ac41ef2 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 09:01:41 -0300 Subject: [PATCH 025/264] Working tags --- client/coral-embed-stream/src/components/Comment.js | 2 +- client/coral-embed-stream/src/containers/Comment.js | 4 ---- client/coral-framework/graphql/fragments/commentView.graphql | 4 +++- .../coral-framework/graphql/mutations/addCommentTag.graphql | 4 +++- .../graphql/mutations/removeCommentTag.graphql | 4 +++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 9d7cb436d..dabac433c 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -25,7 +25,7 @@ import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import {getEditableUntilDate} from './util'; import styles from './Comment.css'; -const isStaff = tags => !tags.every(t => t.id !== 'STAFF'); +const isStaff = tags => !!tags.filter(i => i.tag.name === 'STAFF').length; // hold actions links (e.g. Reply) along the comment footer const ActionButton = ({children}) => { diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 7b5159cc1..7ae96e177 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -30,10 +30,6 @@ export default withFragments({ tags { tag { name - created_at - } - assigned_by { - id } } user { diff --git a/client/coral-framework/graphql/fragments/commentView.graphql b/client/coral-framework/graphql/fragments/commentView.graphql index 1832cc77d..0aa80c9cf 100644 --- a/client/coral-framework/graphql/fragments/commentView.graphql +++ b/client/coral-framework/graphql/fragments/commentView.graphql @@ -6,7 +6,9 @@ fragment commentView on Comment { created_at status tags { - id + tag { + name + } } user { id diff --git a/client/coral-framework/graphql/mutations/addCommentTag.graphql b/client/coral-framework/graphql/mutations/addCommentTag.graphql index a4ff36da4..aa4ce7f5b 100644 --- a/client/coral-framework/graphql/mutations/addCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/addCommentTag.graphql @@ -3,7 +3,9 @@ mutation AddCommentTag ($id: ID!, $tag: String!) { comment { id tags { - id + tag { + name + } } } errors { diff --git a/client/coral-framework/graphql/mutations/removeCommentTag.graphql b/client/coral-framework/graphql/mutations/removeCommentTag.graphql index 642466f00..c89fc24f9 100644 --- a/client/coral-framework/graphql/mutations/removeCommentTag.graphql +++ b/client/coral-framework/graphql/mutations/removeCommentTag.graphql @@ -3,7 +3,9 @@ mutation RemoveCommentTag ($id: ID!, $tag: String!) { comment { id tags { - id + tag { + name + } } } errors { From 54eb078a131e4ac5767e2c2456fd3514eab25b20 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 09:37:26 -0300 Subject: [PATCH 026/264] Working offtopic tag --- graph/mutators/comment.js | 16 ++++++++++++---- .../client/components/OffTopicTag.js | 4 +--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 5c39f7b69..4755aa4ac 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -16,16 +16,24 @@ const Wordlist = require('../../services/wordlist'); * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null}, status = 'NONE') => { +const createComment = ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { + + // Handle Tags + if (!!tags.length) { + tags = tags.map(tag => ({ + tag: { + name: tag + } + })) + } // Add the staff tag for comments created as a staff member. - let tags = []; if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - tags = [{ + tags.push({ tag: { name: 'STAFF' } - }]; + }); } return CommentsService.publicCreate({ diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js index 73e2372ed..0b50eb131 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js @@ -1,9 +1,7 @@ import React from 'react'; import styles from './styles.css'; -const isOffTopic = (tags) => { - return !!tags.filter(tag => tag.name === 'OFF_TOPIC').length -} +const isOffTopic = tags => !!tags.filter(i => i.tag.name === 'OFF_TOPIC').length; export default (props) => ( From 9dc0693530e62efcc10c108c0edb49407b7dd2b6 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 09:43:04 -0300 Subject: [PATCH 027/264] Linting --- graph/mutators/comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 4755aa4ac..615d9cd43 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -19,12 +19,12 @@ const Wordlist = require('../../services/wordlist'); const createComment = ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { // Handle Tags - if (!!tags.length) { + if (tags.length) { tags = tags.map(tag => ({ tag: { name: tag } - })) + })); } // Add the staff tag for comments created as a staff member. From 9db77360b456fd62b26ab3a57f65c49e4fb8fb4f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 17 May 2017 08:16:48 -0600 Subject: [PATCH 028/264] removed unused errors --- errors.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/errors.js b/errors.js index c0d405553..19b0ec90c 100644 --- a/errors.js +++ b/errors.js @@ -75,18 +75,6 @@ const ErrMissingToken = new APIError('token is required', { status: 400 }); -// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. -const ErrNoCommentFound = new APIError('comment does not exist', { - translation_key: 'COMMENT_NOT_FOUND', - status: 400 -}); - -// ErrNoCommentFound is returned when trying to add a tag to a comment that does not exist. -const ErrTagNotAllowed = new APIError('tag not allowed', { - translation_key: 'TAG_NOT_ALLOWED', - status: 400 -}); - // ErrAssetCommentingClosed is returned when a comment or action is attempted on // a stream where commenting has been closed. class ErrAssetCommentingClosed extends APIError { @@ -179,8 +167,6 @@ module.exports = { ErrMissingEmail, ErrMissingPassword, ErrMissingToken, - ErrNoCommentFound, - ErrTagNotAllowed, ErrEmailTaken, ErrSpecialChars, ErrMissingUsername, From b2f1707f1732d92c08c03efb3efe39fb6b8b67a5 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 11:19:06 -0300 Subject: [PATCH 029/264] Fully working offtopic --- client/coral-embed-stream/src/graphql/index.js | 10 +++++++++- client/coral-framework/graphql/mutations.js | 1 - client/coral-plugin-commentbox/actions.js | 4 ++++ client/coral-plugin-commentbox/constants.js | 1 + client/coral-plugin-commentbox/reducer.js | 4 +++- .../client/components/OffTopicCheckbox.js | 14 ++++++++++++-- 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index d43bfd3d5..77041f554 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -170,7 +170,15 @@ const extension = { parent_id, asset_id, action_summaries: [], - tags, + tags: tags.map(tag => ({ + tag: { + name: tag, + created_at: new Date().toISOString(), + }, + assigned_by: { + id: auth.toJS().user.id, + }, + })), status: null, id: 'pending' } diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 66e68be97..1d5318853 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -168,4 +168,3 @@ export const withStopIgnoringUser = withMutation( }); }}), }); - diff --git a/client/coral-plugin-commentbox/actions.js b/client/coral-plugin-commentbox/actions.js index 46bcf4b1b..879824d68 100644 --- a/client/coral-plugin-commentbox/actions.js +++ b/client/coral-plugin-commentbox/actions.js @@ -7,3 +7,7 @@ export const removeTag = idx => ({ type: 'REMOVE_TAG', idx }); + +export const clearTags = () => ({ + type: 'CLEAR_TAGS', +}); diff --git a/client/coral-plugin-commentbox/constants.js b/client/coral-plugin-commentbox/constants.js index b1290f02e..4a80bfe4f 100644 --- a/client/coral-plugin-commentbox/constants.js +++ b/client/coral-plugin-commentbox/constants.js @@ -1,2 +1,3 @@ export const ADD_TAG = 'ADD_TAG'; export const REMOVE_TAG = 'REMOVE_TAG'; +export const CLEAR_TAGS = 'CLEAR_TAGS'; diff --git a/client/coral-plugin-commentbox/reducer.js b/client/coral-plugin-commentbox/reducer.js index d9065fe87..e84f35e93 100644 --- a/client/coral-plugin-commentbox/reducer.js +++ b/client/coral-plugin-commentbox/reducer.js @@ -1,4 +1,4 @@ -import {ADD_TAG, REMOVE_TAG} from './constants'; +import {ADD_TAG, REMOVE_TAG, CLEAR_TAGS} from './constants'; const initialState = { tags: [] @@ -19,6 +19,8 @@ export default function commentBox (state = initialState, action) { ...state.tags.slice(action.idx + 1) ] }; + case CLEAR_TAGS : + return initialState; default : return state; } diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js index 4e7ef7adf..329a16dca 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js @@ -1,13 +1,23 @@ import React from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {addTag, removeTag} from 'coral-plugin-commentbox/actions'; +import {addTag, removeTag, clearTags} from 'coral-plugin-commentbox/actions'; import styles from './styles.css'; class OffTopicCheckbox extends React.Component { label = 'OFF_TOPIC'; + componentDidMount() { + this.clearTagsHook = this.props.registerHook('postSubmit', (data) => { + this.props.clearTags(); + }); + } + + componentWillUnmount() { + this.props.unregisterHook(this.clearTagsHook); + } + handleChange = (e) => { if (e.target.checked) { this.props.addTag(this.label) @@ -33,6 +43,6 @@ class OffTopicCheckbox extends React.Component { const mapStateToProps = ({commentBox}) => ({commentBox}); const mapDispatchToProps = dispatch => - bindActionCreators({addTag, removeTag}, dispatch); + bindActionCreators({addTag, removeTag, clearTags}, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox); From 6664f73d2df567ceb6c2ab8372736c09418e355f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 11:57:51 -0300 Subject: [PATCH 030/264] Best tag working, only Optimistic Missing --- .../src/services/fragmentMatcher.js | 3 +- .../src/components/Comment.js | 22 ++++++------ .../src/components/Stream.js | 12 +++---- .../src/containers/Stream.js | 7 ++-- .../coral-embed-stream/src/graphql/index.js | 35 ++----------------- client/coral-framework/graphql/mutations.js | 28 ++++++++------- .../graphql/mutations/addCommentTag.graphql | 15 -------- .../mutations/removeCommentTag.graphql | 15 -------- client/coral-plugin-best/BestButton.js | 8 ++--- 9 files changed, 44 insertions(+), 101 deletions(-) delete mode 100644 client/coral-framework/graphql/mutations/addCommentTag.graphql delete mode 100644 client/coral-framework/graphql/mutations/removeCommentTag.graphql diff --git a/client/coral-admin/src/services/fragmentMatcher.js b/client/coral-admin/src/services/fragmentMatcher.js index 3b57de0b1..231c0fa2b 100644 --- a/client/coral-admin/src/services/fragmentMatcher.js +++ b/client/coral-admin/src/services/fragmentMatcher.js @@ -26,8 +26,7 @@ const fm = new IntrospectionFragmentMatcher({ {name: 'SetUserStatusResponse'}, {name: 'SuspendUserResponse'}, {name: 'SetCommentStatusResponse'}, - {name: 'AddCommentTagResponse'}, - {name: 'RemoveCommentTagResponse'}, + {name: 'ModifyTagResponse'}, {name: 'IgnoreUserResponse'}, {name: 'StopIgnoringUserResponse'} ] diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index dabac433c..6525f528f 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -109,10 +109,10 @@ class Comment extends React.Component { commentIsIgnored: React.PropTypes.func, // dispatch action to add a tag to a comment - addCommentTag: React.PropTypes.func, + addTag: React.PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: React.PropTypes.func, + removeTag: React.PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, @@ -171,8 +171,8 @@ class Comment extends React.Component { setActiveReplyBox, activeReplyBox, deleteAction, - addCommentTag, - removeCommentTag, + addTag, + removeTag, ignoreUser, disableReply, commentIsIgnored, @@ -213,18 +213,20 @@ class Comment extends React.Component { const addBestTag = notifyOnError( () => - addCommentTag({ + addTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + asset_id: asset.id }), () => 'Failed to tag comment as best' ); const removeBestTag = notifyOnError( () => - removeCommentTag({ + removeTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + asset_id: asset.id }), () => 'Failed to remove best comment tag' ); @@ -394,8 +396,8 @@ class Comment extends React.Component { currentUser={currentUser} postFlag={postFlag} deleteAction={deleteAction} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} charCountEnable={charCountEnable} maxCharCount={maxCharCount} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index b7b325cf6..848427aa3 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -33,8 +33,8 @@ class Stream extends React.Component { loadMore, deleteAction, showSignInDialog, - addCommentTag, - removeCommentTag, + addTag, + removeTag, pluginProps, ignoreUser, auth: {loggedIn, isAdmin, user}, @@ -166,8 +166,8 @@ class Stream extends React.Component { currentUser={user} postFlag={postFlag} postDontAgree={postDontAgree} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} commentIsIgnored={commentIsIgnored} loadMore={loadMore} @@ -201,10 +201,10 @@ Stream.propTypes = { postComment: PropTypes.func.isRequired, // dispatch action to add a tag to a comment - addCommentTag: PropTypes.func, + addTag: PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: PropTypes.func, + removeTag: PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index f19cc0a34..69f3c5491 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -8,7 +8,7 @@ import isNil from 'lodash/isNil'; import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream'; import { withPostComment, withPostFlag, withPostDontAgree, withDeleteAction, - withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment, + withAddTag, withRemoveTag, withIgnoreUser, withEditComment, } from 'coral-framework/graphql/mutations'; import {notificationActions, authActions} from 'coral-framework'; @@ -243,10 +243,9 @@ export default compose( withPostComment, withPostFlag, withPostDontAgree, - withAddCommentTag, - withRemoveCommentTag, + withAddTag, + withRemoveTag, withIgnoreUser, withDeleteAction, withEditComment, )(StreamContainer); - diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 77041f554..201f088dc 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -28,39 +28,8 @@ const extension = { } } `, - RemoveCommentTagResponse: gql` - fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse { - comment { - id - tags { - tag { - name - created_at - } - assigned_by { - id - } - } - } - errors { - translation_key - } - } - `, - AddCommentTagResponse: gql` - fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse { - comment { - id - tags { - tag { - name - created_at - } - assigned_by { - id - } - } - } + ModifyTagResponse: gql` + fragment CoralEmbedStream_ModifyTagResponse on ModifyTagResponse { errors { translation_key } diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 1d5318853..ab07b4441 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -95,39 +95,43 @@ export const withDeleteAction = withMutation( }}), }); -export const withAddCommentTag = withMutation( +export const withAddTag = withMutation( gql` - mutation AddCommentTag($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { - ...AddCommentTagResponse + mutation AddCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { + ...ModifyTagResponse } } `, { props: ({mutate}) => ({ - addCommentTag: ({id, tag}) => { + addTag: ({id, name, asset_id}) => { return mutate({ variables: { id, - tag + name, + asset_id } }); }}), }); -export const withRemoveCommentTag = withMutation( +export const withRemoveTag = withMutation( gql` - mutation RemoveCommentTag($id: ID!, $tag: String!) { - removeCommentTag(id:$id, tag:$tag) { - ...RemoveCommentTagResponse + mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { + errors { + translation_key + } } } `, { props: ({mutate}) => ({ - removeCommentTag: ({id, tag}) => { + removeTag: ({id, name, asset_id}) => { return mutate({ variables: { id, - tag + name, + asset_id } }); }}), diff --git a/client/coral-framework/graphql/mutations/addCommentTag.graphql b/client/coral-framework/graphql/mutations/addCommentTag.graphql deleted file mode 100644 index aa4ce7f5b..000000000 --- a/client/coral-framework/graphql/mutations/addCommentTag.graphql +++ /dev/null @@ -1,15 +0,0 @@ -mutation AddCommentTag ($id: ID!, $tag: String!) { - addCommentTag(id:$id, tag:$tag) { - comment { - id - tags { - tag { - name - } - } - } - errors { - translation_key - } - } -} diff --git a/client/coral-framework/graphql/mutations/removeCommentTag.graphql b/client/coral-framework/graphql/mutations/removeCommentTag.graphql deleted file mode 100644 index c89fc24f9..000000000 --- a/client/coral-framework/graphql/mutations/removeCommentTag.graphql +++ /dev/null @@ -1,15 +0,0 @@ -mutation RemoveCommentTag ($id: ID!, $tag: String!) { - removeCommentTag(id:$id, tag:$tag) { - comment { - id - tags { - tag { - name - } - } - } - errors { - translation_key - } - } -} diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index 96ef45f86..691b3c889 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -6,10 +6,10 @@ import classnames from 'classnames'; // tag string for best comments export const BEST_TAG = 'BEST'; -export const commentIsBest = ({tags} = {}) => { - const isBest = Array.isArray(tags) && tags.some(t => t.name === BEST_TAG); - return isBest; -}; + +export const commentIsBest = ({tags} = {}) => tags.some(t => t.tag.name === BEST_TAG); + +// const commentIsBest = tags => !!tags.filter(i => i.tag.name === 'BEST_TAG).length; const name = 'coral-plugin-best'; const lang = new I18n(translations); From f57fa7cb7bb9753f461c244b956df3a9060efdbb Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 17 May 2017 23:53:07 -0300 Subject: [PATCH 031/264] Update Cache --- .../src/components/Comment.js | 2 +- client/coral-framework/graphql/mutations.js | 49 +++++++++++++++++-- client/coral-plugin-best/BestButton.js | 2 - 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 6525f528f..a0d293bdb 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -216,7 +216,7 @@ class Comment extends React.Component { addTag({ id: comment.id, name: BEST_TAG, - asset_id: asset.id + assetId: asset.id }), () => 'Failed to tag comment as best' ); diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index ab07b4441..2ad2d6f50 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -95,6 +95,16 @@ export const withDeleteAction = withMutation( }}), }); + const COMMENT_FRAGMENT = gql` + fragment CoralRespect_UpdateFragment on Comment { + tags { + tag { + name + } + } + } + `; + export const withAddTag = withMutation( gql` mutation AddCommentTag($id: ID!, $asset_id: ID!, $name: String!) { @@ -104,13 +114,35 @@ export const withAddTag = withMutation( } `, { props: ({mutate}) => ({ - addTag: ({id, name, asset_id}) => { + addTag: ({id, name, assetId}) => { return mutate({ variables: { id, name, - asset_id - } + asset_id: assetId + }, + optimisticResponse: { + deleteAction: { + __typename: 'DeleteActionResponse', + errors: null, + } + }, + update: (proxy) => { + const fragmentId = `Comment_${id}`; + + // Read the data from our cache for this query. + const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); + + data.tags.push({ + tag: { + __typename: 'TagLink', + name + } + }); + + // Write our data back to the cache. + proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); + }, }); }}), }); @@ -132,6 +164,17 @@ export const withRemoveTag = withMutation( id, name, asset_id + }, + update: (proxy) => { + const fragmentId = `Comment_${id}`; + + // Read the data from our cache for this query. + const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); + + console.log('remove', data); + + // Write our data back to the cache. + proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); } }); }}), diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index 691b3c889..b279c0adc 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -9,8 +9,6 @@ export const BEST_TAG = 'BEST'; export const commentIsBest = ({tags} = {}) => tags.some(t => t.tag.name === BEST_TAG); -// const commentIsBest = tags => !!tags.filter(i => i.tag.name === 'BEST_TAG).length; - const name = 'coral-plugin-best'; const lang = new I18n(translations); From 17e7b28688f60e5780906af3b3f8200586a08cbc Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 18 May 2017 00:21:08 -0300 Subject: [PATCH 032/264] Fully working best tag --- client/coral-framework/graphql/mutations.js | 15 +++++++++------ client/coral-plugin-best/BestButton.js | 4 ---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index ab7f4ae78..cf57688ee 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -95,8 +95,8 @@ export const withDeleteAction = withMutation( }}), }); - const COMMENT_FRAGMENT = gql` - fragment CoralRespect_UpdateFragment on Comment { +const COMMENT_FRAGMENT = gql` + fragment CoraBest_UpdateFragment on Comment { tags { tag { name @@ -135,9 +135,10 @@ export const withAddTag = withMutation( data.tags.push({ tag: { - __typename: 'TagLink', - name - } + __typename: 'Tag', + name: "BEST" + }, + __typename: 'TagLink' }); // Write our data back to the cache. @@ -171,7 +172,9 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - console.log('remove', data); + const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); + + data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; // Write our data back to the cache. proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data}); diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index acc971a05..8a17cfdf8 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -6,12 +6,8 @@ import classnames from 'classnames'; // tag string for best comments export const BEST_TAG = 'BEST'; -<<<<<<< HEAD -export const commentIsBest = ({tags} = {}) => tags.some(t => t.tag.name === BEST_TAG); -======= export const commentIsBest = ({tags} = {}) => tags.some((t) => t.tag.name === BEST_TAG); ->>>>>>> 7a256bdd2b62c5bdf6f20df76baa910ab0b44166 const name = 'coral-plugin-best'; const lang = new I18n(translations); From 132b0a854d4654fbf53c2c09964dc30be7f3f118 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 18 May 2017 00:22:39 -0300 Subject: [PATCH 033/264] linting --- client/coral-framework/graphql/mutations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index cf57688ee..6eb1b4bad 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -136,7 +136,7 @@ export const withAddTag = withMutation( data.tags.push({ tag: { __typename: 'Tag', - name: "BEST" + name: 'BEST' }, __typename: 'TagLink' }); @@ -172,7 +172,7 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); + const idx = data.tags.findIndex((i) => i.tag.name === 'BEST'); data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; From 8f219c47a763c6a4db19e06b5ce45288b2dc994e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 19 May 2017 12:23:53 -0600 Subject: [PATCH 034/264] Tag server impl --- PLUGINS.md | 24 ++++++ graph/loaders/index.js | 2 + graph/loaders/tags.js | 22 +++++ graph/mutators/comment.js | 74 +++++++++++----- graph/mutators/tag.js | 60 ++----------- plugins.default.json | 3 +- plugins/coral-plugin-offtopic/index.js | 13 ++- .../server/typeDefs.graphql | 4 - services/tags.js | 85 ++++++++++++++++++- test/server/graph/mutations/addTag.js | 2 +- 10 files changed, 209 insertions(+), 80 deletions(-) create mode 100644 graph/loaders/tags.js delete mode 100644 plugins/coral-plugin-offtopic/server/typeDefs.graphql diff --git a/PLUGINS.md b/PLUGINS.md index 0d17ccd55..4462f8577 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -307,6 +307,30 @@ module.exports = { } ``` +#### Field: `tags` + +The tags hook allows a plugin to define tags that are code controlled (added +or enabled by code). Below is an example pulled from the core off topic plugin +on how to create a hook for the `OFF_TOPIC` name: + +```js +[ + { + name: 'OFF_TOPIC', + permissions: { + public: true, + self: true, + roles: [] + }, + models: ['COMMENTS'], + created_at: new Date() + } +] +``` + +You can refer to `models/schema/tag.js` for the available schema to match when +creating models to enable/disable specific features. + #### Field: `passport` ```js diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 26727940f..8bc170c6d 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -6,6 +6,7 @@ const Assets = require('./assets'); const Comments = require('./comments'); const Metrics = require('./metrics'); const Settings = require('./settings'); +const Tags = require('./tags'); const Users = require('./users'); const plugins = require('../../services/plugins'); @@ -18,6 +19,7 @@ let loaders = [ Comments, Metrics, Settings, + Tags, Users, // Load the plugin loaders from the manager. diff --git a/graph/loaders/tags.js b/graph/loaders/tags.js new file mode 100644 index 000000000..23ab6fd43 --- /dev/null +++ b/graph/loaders/tags.js @@ -0,0 +1,22 @@ +const DataLoader = require('dataloader'); +const TagsService = require('../../services/tags'); + +/** + * Get all the tags for the context for the dataloader. + */ +const genAll = (context, queries) => { + return Promise.all(queries.map(({id, item_type, asset_id}) => { + return TagsService.getAll({id, item_type, asset_id}); + })); +}; + +/** + * Creates a set of loaders based on a GraphQL context. + * @param {Object} context the context of the GraphQL request + * @return {Object} object of loaders + */ +module.exports = (context) => ({ + Tags: { + getAll: new DataLoader((queries) => genAll(context, queries)) + } +}); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 193753275..75e7d9fc0 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -2,11 +2,61 @@ const errors = require('../../errors'); const AssetsService = require('../../services/assets'); const ActionsService = require('../../services/actions'); +const TagsService = require('../../services/tags'); const CommentsService = require('../../services/comments'); const linkify = require('linkify-it')(); - const Wordlist = require('../../services/wordlist'); +const debug = require('debug')('talk:graph:mutators:tags'); +const plugins = require('../../services/plugins'); + +const pluginTags = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => { + debug(`added plugin '${plugin.name}'`); + + acc = acc.concat(tags); + + return acc; +}, []); + +const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => { + const item_type = 'COMMENTS'; + + // Handle Tags + if (tags.length) { + + // Get the global list of tags from the dataloader. + let globalTags = await Tags.getAll.load({ + item_type, + asset_id + }); + if (!Array.isArray(globalTags)) { + globalTags = []; + } + + globalTags = globalTags.concat(pluginTags); + + // Merge in the tags for the given comment. + tags = tags.map((name) => { + + // Resolve the TagLink that we can use for the comment. + let {tagLink} = TagsService.resolveLink(user, globalTags, {name, item_type}); + + // Return the tagLink for tag insertion. + return tagLink; + }); + } + + // Add the staff tag for comments created as a staff member. + if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + tags.push(TagsService.newTagLink(user, { + name: 'STAFF', + item_type + })); + } + + return tags; +}; + /** * Creates a new comment. * @param {Object} user the user performing the request @@ -16,25 +66,11 @@ const Wordlist = require('../../services/wordlist'); * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = async ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { +const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { + const {user, loaders: {Comments}, pubsub} = context; - // Handle Tags - if (tags.length) { - tags = tags.map((tag) => ({ - tag: { - name: tag - } - })); - } - - // Add the staff tag for comments created as a staff member. - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { - tags.push({ - tag: { - name: 'STAFF' - } - }); - } + // Resolve the tags for the comment. + tags = await resolveTagsForComment(context, {asset_id, tags}); let comment = await CommentsService.publicCreate({ body, diff --git a/graph/mutators/tag.js b/graph/mutators/tag.js index 3182a7d00..0c93d8ea0 100644 --- a/graph/mutators/tag.js +++ b/graph/mutators/tag.js @@ -4,62 +4,18 @@ const errors = require('../../errors'); /** * Modifies the targeted model with the specified operation to add/remove a tag. */ -const modify = async ({user}, operation, {name, id, item_type, asset_id}) => { +const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type, asset_id}) => { - // Try to find the tag in the global list. This will contain the permission - // information if it's found. - let tag = await TagsService.get({name, id, item_type, asset_id}); + // Get the global list of tags from the dataloader. + const tags = await Tags.getAll.load({id, item_type, asset_id}); - // Create the new tagLink that will be created to interact to the comment. - let tagLink = { - tag, - assigned_by: user.id, - created_at: new Date() - }; - - // If the tag was found, we need to ensure that the current user can indeed - // modify this tag on the comment. - if (tag) { - - // If the tag has roles defined, and the current user has at least one of - // the required roles, then modify the tag without checking for ownership. - if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { - return operation(id, item_type, tagLink, false); - } - - // If the permissions allow for self assignment, then ensure that the query - // is compose with that in mind. - if (tag.permissions && tag.permissions.self) { - - // Otherwise, we assume that we have to check to see that the user indeed - // owns the resource before allowing the tag to get modified. - return operation(id, item_type, tagLink, true); - } - - throw errors.ErrNotAuthorized; - } - - // Only admin/moderators can modify unique tags, these are tags that are not - // in the global list. - if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { - throw errors.ErrNotAuthorized; - } - - // Generate the tag in the event now that we have to create the tag for this - // specific comment. - tagLink.tag = { - name, - permissions: { - public: true, - self: false, - roles: [] - }, - models: [item_type], - created_at: new Date() - }; + // Resolve the TagLink that should be used to insert to the user. This will + // addtionally return with an ownership property that can be used to determine + // that the user who adds this tag must also be the owner of the resource. + let {tagLink, ownership} = TagsService.resolveLink(user, tags, {name, item_type}); // Actually modify the tag on the model. - return operation(id, item_type, tagLink, false); + return operation(id, item_type, tagLink, ownership); }; module.exports = (context) => { diff --git a/plugins.default.json b/plugins.default.json index b3443af48..7af4eb4fe 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -1,7 +1,8 @@ { "server": [ "coral-plugin-respect", - "coral-plugin-facebook-auth" + "coral-plugin-facebook-auth", + "coral-plugin-offtopic" ], "client": [ "coral-plugin-respect" diff --git a/plugins/coral-plugin-offtopic/index.js b/plugins/coral-plugin-offtopic/index.js index 51ad1c24e..9988686ff 100644 --- a/plugins/coral-plugin-offtopic/index.js +++ b/plugins/coral-plugin-offtopic/index.js @@ -2,5 +2,16 @@ const {readFileSync} = require('fs'); const path = require('path'); module.exports = { - typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8') + tags: [ + { + name: 'OFF_TOPIC', + permissions: { + public: true, + self: true, + roles: [] + }, + models: ['COMMENTS'], + created_at: new Date() + } + ] }; diff --git a/plugins/coral-plugin-offtopic/server/typeDefs.graphql b/plugins/coral-plugin-offtopic/server/typeDefs.graphql deleted file mode 100644 index 48ba577cf..000000000 --- a/plugins/coral-plugin-offtopic/server/typeDefs.graphql +++ /dev/null @@ -1,4 +0,0 @@ -## Extending TAG_TYPE by adding OFF_TOPIC Tag -enum TAG_TYPE { - OFF_TOPIC -} diff --git a/services/tags.js b/services/tags.js index 3ee3de121..7653434c9 100644 --- a/services/tags.js +++ b/services/tags.js @@ -5,6 +5,8 @@ const UserModel = require('../models/user'); const AssetsService = require('./assets'); const SettingsService = require('./settings'); +const errors = require('../errors'); + const updateModel = async (item_type, query, update) => { // Get the model to update with. @@ -43,7 +45,7 @@ class TagsService { /** * Retrives a global tag from the settings based on the input_type. */ - static async get({name, id, item_type, asset_id = null}) { + static async getAll({id, item_type, asset_id = null}) { // Extract the settings from the database. let settings; @@ -66,7 +68,86 @@ class TagsService { let {tags = []} = settings; // Return the first tag that matches the requested form. - return tags.find((tag) => tag.name === name && tag.models.include(item_type)); + return tags; + } + + /** + * Resolves the tagLink and ownership verification requirements that should be + * used when trying to perform tag adding/removing operations. + */ + static resolveLink(user, tags, {name, item_type}) { + + // Try to find the tag in the global list. This will contain the permission + // information if it's found. + let tag = tags.find((tag) => { + return tag.name === name && Array.isArray(tag.models) && tag.models.includes(item_type); + }); + + // Create the new tagLink that will be created to interact to the comment. + let tagLink = { + tag, + assigned_by: user.id, + created_at: new Date() + }; + + // If the tag was found, we need to ensure that the current user can indeed + // modify this tag on the comment. + if (tag) { + + // If the tag has roles defined, and the current user has at least one of + // the required roles, then modify the tag without checking for ownership. + if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) { + return {tagLink, ownership: false}; + } + + // If the permissions allow for self assignment, then ensure that the query + // is compose with that in mind. + if (tag.permissions && tag.permissions.self) { + + // Otherwise, we assume that we have to check to see that the user indeed + // owns the resource before allowing the tag to get modified. + return {tagLink, ownership: true}; + } + + throw errors.ErrNotAuthorized; + } + + // Only admin/moderators can modify unique tags, these are tags that are not + // in the global list. + if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) { + throw errors.ErrNotAuthorized; + } + + // Generate the tag in the event now that we have to create the tag for this + // specific comment. + tagLink = TagsService.newTagLink(user, {name, item_type}); + + // Actually modify the tag on the model. + return {tagLink, ownership: false}; + } + + static newTag({name, item_type}) { + return { + name, + permissions: { + public: true, + self: false, + roles: [] + }, + models: [item_type], + created_at: new Date() + }; + } + + /** + * Creates a new TagLink based on the input user and the tag data. + */ + static newTagLink(user, tag) { + return { + tag: TagsService.newTag(tag), + assigned_by: user.id, + created_at: new Date() + }; } /** diff --git a/test/server/graph/mutations/addTag.js b/test/server/graph/mutations/addTag.js index e6639d49c..17e268cea 100644 --- a/test/server/graph/mutations/addTag.js +++ b/test/server/graph/mutations/addTag.js @@ -12,7 +12,7 @@ describe('graph.mutations.addTag', () => { let comment, asset; beforeEach(async () => { await SettingsService.init(); - + asset = new AssetModel({url: 'http://new.test.com/'}); await asset.save(); From 1e904584e74d79ea1596688b7913f57b4323c1f3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 6 Jun 2017 17:31:52 -0600 Subject: [PATCH 035/264] Added model migrations --- bin/cli | 1 + bin/cli-migration | 88 +++++++++++++++++++++ migrations/1496771633_tags.js | 78 +++++++++++++++++++ models/migration.js | 10 +++ package.json | 1 + services/migration.js | 142 ++++++++++++++++++++++++++++++++++ yarn.lock | 16 ++++ 7 files changed, 336 insertions(+) create mode 100755 bin/cli-migration create mode 100644 migrations/1496771633_tags.js create mode 100644 models/migration.js create mode 100644 services/migration.js diff --git a/bin/cli b/bin/cli index 7e27fa85b..1f3d50ad2 100755 --- a/bin/cli +++ b/bin/cli @@ -13,6 +13,7 @@ program .command('setup', 'setup the application') .command('jobs', 'work with the job queues') .command('users', 'work with the application auth') + .command('migration', 'provides utilities for migrating the database') .command('plugins', 'provides utilities for interacting with the plugin system') .parse(process.argv); diff --git a/bin/cli-migration b/bin/cli-migration new file mode 100755 index 000000000..9c3e077b1 --- /dev/null +++ b/bin/cli-migration @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +const program = require('./commander'); +const util = require('./util'); +const inquirer = require('inquirer'); +const mongoose = require('../services/mongoose'); +const MigrationService = require('../services/migration'); + +// Register shutdown hooks. +util.onshutdown([ + () => mongoose.disconnect() +]); + +async function createMigration(name) { + try { + + // Create the migration. + await MigrationService.create(name); + + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } +} + +async function runMigrations() { + + try { + + // Get the migrations to run. + let migrations = await MigrationService.listPending(); + + console.log('Now going to run the following migrations:\n'); + + for (let {filename} of migrations) { + console.log(`\tmigrations/${filename}`); + } + + let answers = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Proceed with migrations', + default: false + } + ]); + + if (answers.confirm) { + + // Run the migrations. + await MigrationService.run(migrations); + } else { + console.warn('Skipping migrations'); + } + + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } +} + +//============================================================================== +// Setting up the program command line arguments. +//============================================================================== + +program + .command('create ') + .description('creates a new migration') + .action(createMigration); + +program + .command('run') + .description('runs all pending migrations') + .action(runMigrations); + +program.parse(process.argv); + +// If there is no command listed, output help. +if (process.argv.length <= 2) { + program.outputHelp(); + util.shutdown(); +} diff --git a/migrations/1496771633_tags.js b/migrations/1496771633_tags.js new file mode 100644 index 000000000..9b2aeedf0 --- /dev/null +++ b/migrations/1496771633_tags.js @@ -0,0 +1,78 @@ +const CommentModel = require('../models/comment'); + +module.exports = { + async up() { + + // Find all comments that have tags. + let comments = await CommentModel.aggregate([ + {$match: { + tags: { + $exists: true, + $ne: [] + } + }}, + {$project: { + id: true, + tags: true + }} + ]); + + // Create a new batch operation. + let batch = CommentModel.collection.initializeUnorderedBulkOp(); + + // Loop over the comments retrieved, updating the tag structure. + for (let {id, tags} of comments) { + + // OLD + // + // [ + // { + // name: 'OFF_TOPIC', + // assigned_by: '', + // created_at: new Date() + // } + // ] + + // NEW + // + // [ + // { + // tag: { + // name: 'OFF_TOPIC', + // permissions: { + // public: true, + // self: false, + // roles: [] + // }, + // models: ['COMMENTS'], + // created_at: new Date() + // }, + // assigned_by: '', + // created_at: new Date() + // } + // ] + + // Remap the tag structure. + tags = tags.map(({name, assigned_by, created_at}) => ({ + tag: { + name, + permissions: { + public: true, + self: name === 'OFF_TOPIC', // at the time of migration, only off topic tags were self assigning + roles: [] + }, + models: ['COMMENTS'], + created_at + }, + assigned_by, + created_at + })); + + // Execute the batch operation. + batch.find({id}).updateOne({$set: {tags}}); + } + + // Execute the batch update operation. + await batch.execute(); + } +}; diff --git a/models/migration.js b/models/migration.js new file mode 100644 index 000000000..648c5594b --- /dev/null +++ b/models/migration.js @@ -0,0 +1,10 @@ +const mongoose = require('../services/mongoose'); +const Schema = mongoose.Schema; + +const MigrationSchema = new Schema({ + version: Number +}); + +const Migration = mongoose.model('Migration', MigrationSchema); + +module.exports = Migration; diff --git a/package.json b/package.json index 84d0fdf52..450764c90 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "resolve": "^1.3.2", "semver": "^5.3.0", "simplemde": "^1.11.2", + "snake-case": "^2.1.0", "subscriptions-transport-ws": "^0.5.5-alpha.0", "timekeeper": "^1.0.0", "uuid": "^3.0.1" diff --git a/services/migration.js b/services/migration.js new file mode 100644 index 000000000..bc2274de4 --- /dev/null +++ b/services/migration.js @@ -0,0 +1,142 @@ +const MigrationModel = require('../models/migration'); +const fs = require('fs'); +const path = require('path'); +const Joi = require('joi'); +const sc = require('snake-case'); + +const migrationTemplate = ` +module.exports = { + async up() { + + } +}; + +`; + +module.exports = class MigrationService { + static async create(name) { + if (!name || typeof name !== 'string' || name.length === 0) { + throw new Error('name must be defined'); + } + + let version = Math.round(Date.now() / 1000, 0); + let filename = path.join(__dirname, '..', 'migrations', `${version}_${sc(name)}.js`); + fs.writeFileSync(filename, migrationTemplate, 'utf8'); + + console.log(`Created migration ${version} in ${filename}`); + } + + static async listPending() { + + // Get all the migration files. + let migrationFiles = fs.readdirSync(path.join(__dirname, '..', 'migrations')); + + // Ensure that all migrations follow this format. + const migrationSchema = Joi.object({ + up: Joi.func().required(), + down: Joi.func() + }); + + // Extract the version from the filename with this regex. + const versionRe = /(\d+)_([\S_]+)\.js/; + + // Get the latest version. + let latestVersion = await MigrationService.latestVersion(); + + // Parse the migrations from the file listing. + let migrations = migrationFiles + .map((filename) => { + + // Parse the version from the filename. + let matches = filename.match(versionRe); + if (matches.length !== 3) { + return null; + } + let version = parseInt(matches[1]); + + // Don't rerun migrations from versions already ran. + if (version <= latestVersion) { + return null; + } + + // Read the migration from the filesystem. + let migration = require(`../migrations/${filename}`); + Joi.assert(migration, migrationSchema, `Migration ${filename} does did not pass validation`); + + return { + filename, + version, + migration + }; + }) + .filter((migration) => migration !== null) + .sort((a, b) => { + if (a.version < b.version) { + return -1; + } + + if (a.version > b.version) { + return 1; + } + + return 0; + }); + + return migrations; + } + + static async run(migrations) { + if (migrations.length === 0) { + console.log('No migrations to run!'); + return; + } + + for (let {version, migration} of migrations) { + console.log(`Starting migration ${version}.js`); + await migration.up(); + console.log(`Finished migration ${version}.js`); + + console.log(`Recording migration ${version}.js`); + + // Record that the migration was finished. + let m = new MigrationModel({version}); + await m.save(); + + console.log(`Finished recording migration ${version}.js`); + } + } + + static async latestVersion() { + + // Load the latest migration details from the database. + let latestMigration = await MigrationModel + .find({}) + .sort({version: -1}) + .limit(1) + .exec(); + + // If there weren't any migrations at all, then error out. + if (latestMigration.length === 0) { + return null; + } + + // If the latest migration does not match the required version, then error + // out. + return latestMigration[0].version; + } + + static async verify(requiredVersion) { + + // If the requiredVersion isn't specified or is 0, then don't complain. + if (typeof requiredVersion !== 'number' || requiredVersion === 0) { + return; + } + + // If the latest migration does not match the required version, then error + // out. + let latestVersion = await MigrationService.latestVersion(); + if (!latestVersion || latestVersion < requiredVersion) { + throw new Error(`A database migration is required, version required ${requiredVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); + } + } +}; diff --git a/yarn.lock b/yarn.lock index 728687248..adf0675d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5028,6 +5028,10 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: dependencies: js-tokens "^3.0.0" +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -5409,6 +5413,12 @@ nightwatch@^0.9.11: proxy-agent "2.0.0" q "1.4.1" +no-case@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" + dependencies: + lower-case "^1.1.1" + nocache@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980" @@ -7533,6 +7543,12 @@ smtp-connection@2.12.0: httpntlm "1.6.1" nodemailer-shared "1.1.0" +snake-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + dependencies: + no-case "^2.2.0" + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" From cf82f5b7079e41e36d4db3d569a2451b922b5aaf Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 6 Jun 2017 17:32:52 -0600 Subject: [PATCH 036/264] fixed enum --- bin/cli-setup | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/cli-setup b/bin/cli-setup index 1d6de4090..e9f0959ef 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -8,6 +8,7 @@ const program = require('./commander'); const inquirer = require('inquirer'); const mongoose = require('../services/mongoose'); const SettingModel = require('../models/setting'); +const MODERATION_OPTIONS = require('../models/enum/moderation_options'); const SettingsService = require('../services/settings'); const SetupService = require('../services/setup'); const UsersService = require('../services/users'); @@ -90,7 +91,7 @@ const performSetup = () => { }, { type: 'list', - choices: SettingModel.MODERATION_OPTIONS, + choices: MODERATION_OPTIONS, name: 'moderation', default: settings.moderation, message: 'Select a moderation mode' From 6827ff2f1fd723145c9e05d13523d7a68c4fa2a7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 7 Jun 2017 10:27:42 -0600 Subject: [PATCH 037/264] Added support for version enforcing --- migrations/1496771633_tags.js | 5 +++ package.json | 5 +++ services/cache.js | 2 +- services/karma.js | 2 +- services/migration.js | 74 +++++++++++++++++++++++++++++------ services/mongoose.js | 1 + services/passport.js | 2 +- services/redis.js | 2 +- 8 files changed, 76 insertions(+), 17 deletions(-) diff --git a/migrations/1496771633_tags.js b/migrations/1496771633_tags.js index 9b2aeedf0..be3fa2577 100644 --- a/migrations/1496771633_tags.js +++ b/migrations/1496771633_tags.js @@ -17,6 +17,11 @@ module.exports = { }} ]); + // If no comments were found, nothing needes to be done! + if (comments.length <= 0) { + return; + } + // Create a new batch operation. let batch = CommentModel.collection.initializeUnorderedBulkOp(); diff --git a/package.json b/package.json index 450764c90..137249884 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,11 @@ "embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs", "heroku-postbuild": "./bin/cli plugins reconcile && yarn build" }, + "talk": { + "migration": { + "minVersion": 1496771633 + } + }, "config": { "pre-git": { "commit-msg": [], diff --git a/services/cache.js b/services/cache.js index 92da6df0a..25642e1d0 100644 --- a/services/cache.js +++ b/services/cache.js @@ -1,5 +1,5 @@ const redis = require('./redis'); -const debug = require('debug')('talk:cache'); +const debug = require('debug')('talk:services:cache'); const crypto = require('crypto'); const cache = module.exports = { diff --git a/services/karma.js b/services/karma.js index ea932c86a..c76bcebb3 100644 --- a/services/karma.js +++ b/services/karma.js @@ -1,4 +1,4 @@ -const debug = require('debug')('talk:trust'); +const debug = require('debug')('talk:services:karma'); const UserModel = require('../models/user'); /** diff --git a/services/migration.js b/services/migration.js index bc2274de4..11eec3247 100644 --- a/services/migration.js +++ b/services/migration.js @@ -2,10 +2,11 @@ const MigrationModel = require('../models/migration'); const fs = require('fs'); const path = require('path'); const Joi = require('joi'); +const debug = require('debug')('talk:services:migration'); const sc = require('snake-case'); +const {talk: {migration: {minVersion}}} = require('../package.json'); -const migrationTemplate = ` -module.exports = { +const migrationTemplate = `module.exports = { async up() { } @@ -13,12 +14,19 @@ module.exports = { `; -module.exports = class MigrationService { +class MigrationService { + + /** + * Creates a new migration file. + * + * @param {String} name name of the migration + */ static async create(name) { if (!name || typeof name !== 'string' || name.length === 0) { throw new Error('name must be defined'); } + // Create a new Migration based on the current time. let version = Math.round(Date.now() / 1000, 0); let filename = path.join(__dirname, '..', 'migrations', `${version}_${sc(name)}.js`); fs.writeFileSync(filename, migrationTemplate, 'utf8'); @@ -26,6 +34,9 @@ module.exports = class MigrationService { console.log(`Created migration ${version} in ${filename}`); } + /** + * Returns a list of all pending migrations. + */ static async listPending() { // Get all the migration files. @@ -85,27 +96,48 @@ module.exports = class MigrationService { return migrations; } + /** + * Runs an list of migrations. + * + * @param {Array} migrations a list of migrations returned by `listPending` + */ static async run(migrations) { if (migrations.length === 0) { console.log('No migrations to run!'); return; } - for (let {version, migration} of migrations) { - console.log(`Starting migration ${version}.js`); - await migration.up(); - console.log(`Finished migration ${version}.js`); + for (let {filename, version, migration} of migrations) { + try { + console.log(`Starting migration ${filename}`); + await migration.up(); + console.log(`Finished migration ${filename}`); + } catch (e) { + console.error(`Migration ${filename} failed`); + throw e; + } - console.log(`Recording migration ${version}.js`); + try { + console.log(`Recording migration ${filename}`); - // Record that the migration was finished. - let m = new MigrationModel({version}); - await m.save(); + // Record that the migration was finished. + let m = new MigrationModel({version}); + await m.save(); - console.log(`Finished recording migration ${version}.js`); + console.log(`Finished recording migration ${filename}`); + } catch (e) { + console.error(`Migration ${filename} could not be recorded`); + throw e; + } } + + console.log(`Database now at migration version ${migrations[migrations.length - 1].version}`); } + /** + * Returns the latest migration version number that has been applied to the + * database, null if none were found. + */ static async latestVersion() { // Load the latest migration details from the database. @@ -138,5 +170,21 @@ module.exports = class MigrationService { if (!latestVersion || latestVersion < requiredVersion) { throw new Error(`A database migration is required, version required ${requiredVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); } + + return latestVersion; } -}; +} + +// Verify that the minimum migration version is met. +MigrationService + .verify(minVersion) + .then((latestVersion) => { + debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`); + }) + .catch((e) => { + + // Throw the error in the catch block. + throw e; + }); + +module.exports = MigrationService; diff --git a/services/mongoose.js b/services/mongoose.js index acda2f564..c697e38ef 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -59,3 +59,4 @@ require('../models/asset'); require('../models/comment'); require('../models/setting'); require('../models/user'); +require('./migration'); diff --git a/services/passport.js b/services/passport.js index c64bb2334..826297ae5 100644 --- a/services/passport.js +++ b/services/passport.js @@ -7,7 +7,7 @@ const JWT = require('jsonwebtoken'); const LocalStrategy = require('passport-local').Strategy; const errors = require('../errors'); const uuid = require('uuid'); -const debug = require('debug')('talk:passport'); +const debug = require('debug')('talk:services:passport'); const {createClient} = require('./redis'); // Create a redis client to use for authentication. diff --git a/services/redis.js b/services/redis.js index c049b2d00..c6506eb3c 100644 --- a/services/redis.js +++ b/services/redis.js @@ -1,5 +1,5 @@ const redis = require('redis'); -const debug = require('debug')('talk:redis'); +const debug = require('debug')('talk:services:redis'); const { REDIS_URL } = require('../config'); From 4cbc8b2226ffae6c724630acf8d9765c8ffed752 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 7 Jun 2017 11:08:39 -0600 Subject: [PATCH 038/264] Updates to setup process --- bin/cli-serve | 13 +- bin/cli-setup | 309 ++++++++++++++++++++++-------------------- services/migration.js | 22 +-- 3 files changed, 183 insertions(+), 161 deletions(-) diff --git a/bin/cli-serve b/bin/cli-serve index 7978c6c85..63eda3af2 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -5,6 +5,7 @@ const app = require('../app'); const {createServer} = require('http'); const scraper = require('../services/scraper'); const mailer = require('../services/mailer'); +const MigrationService = require('../services/migration'); const kue = require('../services/kue'); const mongoose = require('../services/mongoose'); const util = require('./util'); @@ -87,7 +88,17 @@ function onListening() { /** * Start the app. */ -function startApp(program) { +async function startApp(program) { + + try { + + // Verify that the minimum migration version is met. + await MigrationService.verify(); + + } catch(e) { + console.error(e); + process.exit(1); + } /** * Listen on provided port, on all network interfaces. diff --git a/bin/cli-setup b/bin/cli-setup index e9f0959ef..add134209 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -12,6 +12,7 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options'); const SettingsService = require('../services/settings'); const SetupService = require('../services/setup'); const UsersService = require('../services/users'); +const MigrationService = require('../services/migration'); const util = require('./util'); const errors = require('../errors'); @@ -33,166 +34,186 @@ program // Setup the application //============================================================================== -const performSetup = () => { +const performSetup = async () => { if (program.defaults) { - return SettingsService - .init() - .then(() => { - console.log('Settings created.'); - console.log('\nTalk is now installed!'); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); + await SettingsService.init(); + + console.log('Settings created.'); + console.log('\nTalk is now installed!'); + + return; } // Get the current settings, we are expecing an error here. - return SettingsService - .retrieve() - .then(() => { + try { - // We should NOT have gotten a settings object, this means that the - // application is already setup. Error out here. - throw errors.ErrSettingsInit; + // Try to get the settings. + await SettingsService.retrieve(); - }) - .catch((err) => { + // We should NOT have gotten a settings object, this means that the + // application is already setup. Error out here. + throw errors.ErrSettingsInit; - // If the error is `not init`, then we're good, otherwise, it's something - // else. - if (err !== errors.ErrSettingsNotInit) { - throw err; + } catch (e) { + + // If the error is `not init`, then we're good, otherwise, it's something + // else. + if (e !== errors.ErrSettingsNotInit) { + throw e; + } + } + + // Get the migrations to run. + let migrations = await MigrationService.listPending(); + if (migrations.length > 0) { + + console.log('Now going to run the following migrations:\n'); + + for (let {filename} of migrations) { + console.log(`\tmigrations/${filename}`); + } + + console.log(''); + + let {confirm} = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Proceed with migrations', + default: false } + ]); - }) - .then(() => { + if (!confirm) { + throw new Error('migrations are needed to complete setup'); + } - // Create the base settings model. - let settings = new SettingModel(); + // Perform all migrations. + await MigrationService.run(migrations); + } else { + console.log('No migrations have to be run.'); + } - console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n'); + // Create the base settings model. + let settings = new SettingModel(); - return inquirer.prompt([ - { - type: 'input', - name: 'organizationName', - message: 'Organization Name', - default: settings.organizationName, - validate: (input) => { - if (input && input.length > 0) { - return true; - } + console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n'); - return 'Organization Name is required.'; - } - }, - { - type: 'list', - choices: MODERATION_OPTIONS, - name: 'moderation', - default: settings.moderation, - message: 'Select a moderation mode' - }, - { - type: 'confirm', - name: 'requireEmailConfirmation', - default: settings.requireEmailConfirmation, - message: 'Should emails always be confirmed' - } - ]) - .then((answers) => { - - // Update the settings that were changed. - Object.keys(answers).forEach((key) => { - if (answers[key] !== undefined) { - settings[key] = answers[key]; - } - }); - - console.log('\nWe\'ll ask you some questions about your first admin user.\n'); - - return inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'Username', - filter: (username) => { - return UsersService - .isValidUsername(username, false) - .catch((err) => { - throw err.message; - }); - } - }, - { - name: 'email', - message: 'Email', - format: 'email', - validate: (value) => { - if (value && value.length >= 3) { - return true; - } - - return 'Email is required'; - } - }, - { - name: 'password', - message: 'Password', - type: 'password', - filter: (password) => { - return UsersService - .isValidPassword(password) - .catch((err) => { - throw err.message; - }); - } - }, - { - name: 'confirmPassword', - message: 'Confirm Password', - type: 'password', - filter: (confirmPassword) => { - return UsersService - .isValidPassword(confirmPassword) - .catch((err) => { - throw err.message; - }); - } - }, - ]); - }) - .then((user) => { - - if (user.password !== user.confirmPassword) { - return Promise.reject(new Error('Passwords do not match')); + let answers = await inquirer.prompt([ + { + type: 'input', + name: 'organizationName', + message: 'Organization Name', + default: settings.organizationName, + validate: (input) => { + if (input && input.length > 0) { + return true; } - return SetupService.setup({ - settings: settings.toObject(), - user: { - email: user.email, - username: user.username, - password: user.password - } - }); - }); - }) - .then(({user}) => { - console.log('Settings created.'); - console.log(`User ${user.id} created.`); - console.log('\nTalk is now installed!'); - console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); + return 'Organization Name is required.'; + } + }, + { + type: 'list', + choices: MODERATION_OPTIONS, + name: 'moderation', + default: settings.moderation, + message: 'Select a moderation mode' + }, + { + type: 'confirm', + name: 'requireEmailConfirmation', + default: settings.requireEmailConfirmation, + message: 'Should emails always be confirmed' + } + ]); + + // Update the settings that were changed. + Object.keys(answers).forEach((key) => { + if (answers[key] !== undefined) { + settings[key] = answers[key]; + } + }); + + console.log('\nWe\'ll ask you some questions about your first admin user.\n'); + + let user = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: 'Username', + filter: (username) => { + return UsersService + .isValidUsername(username, false) + .catch((err) => { + throw err.message; + }); + } + }, + { + name: 'email', + message: 'Email', + format: 'email', + validate: (value) => { + if (value && value.length >= 3) { + return true; + } + + return 'Email is required'; + } + }, + { + name: 'password', + message: 'Password', + type: 'password', + filter: (password) => { + return UsersService + .isValidPassword(password) + .catch((err) => { + throw err.message; + }); + } + }, + { + name: 'confirmPassword', + message: 'Confirm Password', + type: 'password', + filter: (confirmPassword) => { + return UsersService + .isValidPassword(confirmPassword) + .catch((err) => { + throw err.message; + }); + } + }, + ]); + + if (user.password !== user.confirmPassword) { + return Promise.reject(new Error('Passwords do not match')); + } + + let {user: newUser} = await SetupService.setup({ + settings: settings.toObject(), + user: { + email: user.email, + username: user.username, + password: user.password + } + }); + + console.log('Settings created.'); + console.log(`User ${newUser.id} created.`); + console.log('\nTalk is now installed!'); + console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'); }; // Start tthe setup process. -performSetup(); +performSetup() + .then(() => { + util.shutdown(); + }) + .catch((e) => { + console.error(e); + util.shutdown(1); + }); diff --git a/services/migration.js b/services/migration.js index 11eec3247..8676b91be 100644 --- a/services/migration.js +++ b/services/migration.js @@ -157,34 +157,24 @@ class MigrationService { return latestMigration[0].version; } - static async verify(requiredVersion) { + static async verify() { // If the requiredVersion isn't specified or is 0, then don't complain. - if (typeof requiredVersion !== 'number' || requiredVersion === 0) { + if (typeof minVersion !== 'number' || minVersion === 0) { return; } // If the latest migration does not match the required version, then error // out. let latestVersion = await MigrationService.latestVersion(); - if (!latestVersion || latestVersion < requiredVersion) { - throw new Error(`A database migration is required, version required ${requiredVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); + if (!latestVersion || latestVersion < minVersion) { + throw new Error(`A database migration is required, version required ${minVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``); } + debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`); + return latestVersion; } } -// Verify that the minimum migration version is met. -MigrationService - .verify(minVersion) - .then((latestVersion) => { - debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`); - }) - .catch((e) => { - - // Throw the error in the catch block. - throw e; - }); - module.exports = MigrationService; From 9818b851db907b0757739470d1eb95f5ce146160 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 7 Jun 2017 15:40:36 -0600 Subject: [PATCH 039/264] clean up the ModerationHeader component --- .../Moderation/components/Moderation.js | 6 ++- .../Moderation/components/ModerationHeader.js | 48 +++++++++++-------- .../routes/Moderation/components/styles.css | 5 ++ 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index c788da993..3efe48bdc 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -32,6 +32,10 @@ export default class Moderation extends Component { this.toggleModal(false); } + openSearch = () => { + + } + moderate = (accept) => () => { const {acceptComment, rejectComment} = this.props; const {selectedIndex} = this.state; @@ -128,7 +132,7 @@ export default class Moderation extends Component { return (
    - + ( +const ModerationHeader = (props) => { + + const allStreams = props.asset + ? {t('modqueue.all_streams')} + : ; + + const title = props.asset + ? ( + + {props.asset.title} + + ) + : {t('modqueue.all_streams')} ; + + return (
    - { - props.asset ? -
    - {t('modqueue.all_streams')} - - {props.asset.title} - - - Select Stream -
    - : -
    - - {t('modqueue.all_streams')} - {t('modqueue.select_stream')} -
    - } +
    + {allStreams} + {title} + {t('modqueue.select_stream')} +
    -); + ); +}; + +ModerationHeader.propTypes = { + openSearch: PropTypes.func.isRequired +}; + export default ModerationHeader; diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index d10311f74..b0c2cc5ac 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -467,3 +467,8 @@ span { .bulkSelectInput { cursor: pointer; } + +.searchTrigger { + position: relative; + top: .3em; +} From 06da4d7d269eb50a7eff200c1262b4c89ebce428 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 7 Jun 2017 16:36:27 -0600 Subject: [PATCH 040/264] toggle story search --- client/coral-admin/src/actions/moderation.js | 2 ++ .../coral-admin/src/constants/moderation.js | 2 ++ client/coral-admin/src/reducers/moderation.js | 6 +++++ .../Moderation/components/Moderation.js | 14 +++++++++-- .../Moderation/components/ModerationHeader.js | 24 +++++++++++-------- .../routes/Moderation/components/styles.css | 1 + .../Moderation/containers/Moderation.js | 2 ++ 7 files changed, 39 insertions(+), 12 deletions(-) diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 37e09590d..9f1c40c94 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -51,3 +51,5 @@ export const toggleSelectCommentInUserDetail = (id, active) => { id }; }; + +export const toggleStorySearch = (active) => ({type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH}); diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index 374de616a..29cf03143 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -12,3 +12,5 @@ export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES'; export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT'; export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT'; export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS'; +export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH'; +export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH'; diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index 1b95bdb90..4bab9eda1 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -12,6 +12,8 @@ const initialState = fromJS({ userDetailStatuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'], userDetailSelectedIds: new Set(), banDialog: false, + storySearchVisible: false, + storySearchString: '', shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show', sortOrder: 'REVERSE_CHRONOLOGICAL', suspendUserDialog: { @@ -80,6 +82,10 @@ export default function moderation (state = initialState, action) { return state.update('userDetailSelectedIds', (set) => set.add(action.id)); case actions.UNSELECT_USER_DETAIL_COMMENT: return state.update('userDetailSelectedIds', (set) => set.delete(action.id)); + case actions.SHOW_STORY_SEARCH: + return state.set('storySearchVisible', true); + case actions.HIDE_STORY_SEARCH: + return state.set('storySearchVisible', false); case actions.SET_SORT_ORDER: return state.set('sortOrder', action.order); default : diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index 3efe48bdc..f7338cf6b 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -32,8 +32,14 @@ export default class Moderation extends Component { this.toggleModal(false); } - openSearch = () => { + closeSearch = () => { + console.log('closeSearch'); + this.props.toggleStorySearch(false); + } + openSearch = () => { + console.log('openSearch'); + this.props.toggleStorySearch(true); } moderate = (accept) => () => { @@ -132,7 +138,11 @@ export default class Moderation extends Component { return (
    - + { +const ModerationHeader = ({asset, searchVisible, openSearch, closeSearch}) => { - const allStreams = props.asset + const trigger = searchVisible ? closeSearch : openSearch; + const searchTriggerIcon = ; + + const allStreams = asset ? {t('modqueue.all_streams')} : ; - const title = props.asset - ? ( - - {props.asset.title} - - ) - : {t('modqueue.all_streams')} ; + const title = asset + ? {asset.title} {searchTriggerIcon} + : {t('modqueue.all_streams')} {searchTriggerIcon}; return (
    @@ -32,7 +31,12 @@ const ModerationHeader = (props) => { }; ModerationHeader.propTypes = { - openSearch: PropTypes.func.isRequired + asset: PropTypes.shape({ + title: PropTypes.string, + id: PropTypes.string + }), + openSearch: PropTypes.func.isRequired, + closeSearch: PropTypes.func.isRequired }; export default ModerationHeader; diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index b0c2cc5ac..a6bf7e89e 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -471,4 +471,5 @@ span { .searchTrigger { position: relative; top: .3em; + background-color: blue; } diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 537aa1d65..31ac05450 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -21,6 +21,7 @@ import { showSuspendUserDialog, hideSuspendUserDialog, hideShortcutsNote, + toggleStorySearch, viewUserDetail, hideUserDetail, setSortOrder, @@ -299,6 +300,7 @@ const mapDispatchToProps = (dispatch) => ({ showBanUserDialog, hideBanUserDialog, hideShortcutsNote, + toggleStorySearch, showSuspendUserDialog, hideSuspendUserDialog, viewUserDetail, From 5d28f611b272a09bbec5bd605469874cc61250ac Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 7 Jun 2017 20:26:48 -0600 Subject: [PATCH 041/264] support for targeting new targets --- plugins.js | 91 +++++++++++++++++++++++++++++------------------ webpack.config.js | 15 +++++++- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/plugins.js b/plugins.js index f0c31407f..4403f89bb 100644 --- a/plugins.js +++ b/plugins.js @@ -78,7 +78,11 @@ function isInternal(name) { */ function pluginPath(name) { if (isInternal(name)) { - return path.join(__dirname, 'plugins', name); + try { + return resolve.sync(name, {moduleDirectory: 'plugins', basedir: process.cwd()}); + } catch (e) { + return undefined; + } } try { @@ -88,15 +92,8 @@ function pluginPath(name) { } } -/** - * Itterates over the plugins and gets the plugin path's, version, and name. - * - * @param {Array} plugins - * @returns {Array} - */ -function itteratePlugins(plugins) { - return plugins.map((p) => { - let plugin = {}; +class Plugin { + constructor(entry) { // This checks to see if the structure for this entry is an object: // @@ -106,23 +103,47 @@ function itteratePlugins(plugins) { // // "people" // - if (typeof p === 'object') { - plugin.name = Object.keys(p).find((name) => name !== null); - plugin.version = p[plugin.name]; - } else if (typeof p === 'string') { - plugin.name = p; - plugin.version = `file:./plugins/${plugin.name}`; + if (typeof entry === 'object') { + this.name = Object.keys(entry).find((name) => name !== null); + this.version = entry[this.name]; + } else if (typeof entry === 'string') { + this.name = entry; + this.version = `file:./plugins/${this.name}`; } else { - throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof p}`); + throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof entry}`); } // Get the path for the plugin. - plugin.path = pluginPath(plugin.name); + this.path = pluginPath(this.name); + } - return plugin; - }); + require() { + if (typeof this.path === 'undefined') { + throw new Error(`plugin '${this.name}' is not local and is not resolvable, plugin reconsiliation may be required`); + } + + try { + this.module = require(this.path); + } catch (e) { + if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(this.name)) { + console.error(new Error(`plugin '${this.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`)); + throw e; + } + + console.error(new Error(`plugin '${this.name}' could not be required from '${this.path}': ${e.message}`)); + throw e; + } + } } +/** + * Itterates over the plugins and gets the plugin path's, version, and name. + * + * @param {Array} plugins + * @returns {Array} + */ +const itteratePlugins = (plugins) => plugins.map((p) => new Plugin(p)); + // Add each plugin folder to the allowed import path so that they can import our // internal dependancies. Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((plugin) => { @@ -139,22 +160,20 @@ Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((p */ class PluginSection { constructor(plugins) { - this.plugins = itteratePlugins(plugins).map((plugin) => { - if (typeof plugin.path === 'undefined') { - throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`); - } + this.required = false; + this.plugins = itteratePlugins(plugins); + } - try { - plugin.module = require(plugin.path); - } catch (e) { - if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(plugin.name)) { - console.error(new Error(`plugin '${plugin.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`)); - throw e; - } + require() { + if (this.required) { + return; + } + + this.required = true; + this.plugins.forEach((plugin) => { - console.error(new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`)); - throw e; - } + // Load the plugin. + plugin.require(); if (isInternal(plugin.name)) { debug(`loading internal plugin '${plugin.name}' from '${plugin.path}'`); @@ -171,6 +190,10 @@ class PluginSection { * available. */ hook(hook) { + + // Load the plugin source if we haven't already. + this.require(); + return this.plugins .filter(({module}) => hook in module) .filter((plugin) => { diff --git a/webpack.config.js b/webpack.config.js index ddd2f9577..d2cd07300 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,6 +10,11 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); +const {plugins, PluginManager} = require('./plugins'); + +const pluginManager = new PluginManager(plugins); +const targetPlugins = pluginManager.section('targets').plugins; + let pluginsConfigPath; let envPlugins = path.join(__dirname, 'plugins.env.js'); @@ -61,6 +66,13 @@ const config = { path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') ]; + return entry; + }, {}), targetPlugins.reduce((entry, plugin) => { + entry[`${plugin.name}/bundle`] = [ + 'babel-polyfill', + plugin.path + ]; + return entry; }, {})), output: { @@ -127,7 +139,8 @@ const config = { plugins: [ new LicenseWebpackPlugin({ pattern: /^(MIT|ISC|BSD.*)$/, - addUrl: true + addUrl: true, + suppressErrors: true }), new Copy([ ...buildEmbeds.map((embed) => ({ From fb39446c6af9fed207f52d24cca90340d092b6ee Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 8 Jun 2017 18:28:46 +0700 Subject: [PATCH 042/264] Port perm checks to the new framework --- .../coral-embed-stream/src/components/Comment.js | 16 ++++++++++++++-- .../coral-embed-stream/src/components/Stream.js | 1 + client/coral-framework/hocs/withReaction.js | 13 +++++-------- client/coral-framework/services/perms.js | 6 ++++++ client/coral-plugin-flags/FlagButton.js | 11 +++++++---- client/coral-plugin-replies/ReplyButton.js | 5 ++--- plugin-api/beta/client/services/index.js | 1 + .../client/components/FakeComment.js | 1 - plugins/coral-plugin-love/client/LoveButton.js | 12 ++++++------ 9 files changed, 42 insertions(+), 24 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index b5eca4ff2..2a12139b5 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -7,6 +7,7 @@ import Content from 'coral-plugin-commentcontent/CommentContent'; import PubDate from 'coral-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; import FlagComment from 'coral-plugin-flags/FlagComment'; +import {can} from 'coral-framework/services/perms'; import {TransitionGroup} from 'react-transition-group'; import cn from 'classnames'; @@ -219,6 +220,18 @@ export default class Comment extends React.Component { this.setState(resetCursors); }; + showReplyBox = () => { + if (!this.props.currentUser) { + this.props.showSignInDialog(); + return; + } + if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { + this.props.setActiveReplyBox(this.props.comment.id); + return; + } + return; + } + // getVisibileReplies returns a list containing comments // which were authored by current user or comes before the `idCursor`. getVisibileReplies() { @@ -477,10 +490,9 @@ export default class Comment extends React.Component { {!disableReply && setActiveReplyBox(comment.id)} + onClick={this.showReplyBox} parentCommentId={parentId || comment.id} currentUserId={currentUser && currentUser.id} - banned={false} /> } (WrappedComponent) => { } ); + const mapStateToProps = (state) => ({ + user: state.auth.toJS().user, + }); + const mapDispatchToProps = (dispatch) => bindActionCreators({showSignInDialog}, dispatch); const enhance = compose( withFragments({ - root: gql` - fragment ${capitalize(reaction)}Button_root on RootQuery { - me { - status - } - } - `, comment: gql` fragment ${capitalize(reaction)}Button_comment on Comment { action_summaries { @@ -232,7 +229,7 @@ export default (reaction) => (WrappedComponent) => { } }` }), - connect(null, mapDispatchToProps), + connect(mapStateToProps, mapDispatchToProps), withDeleteReaction, withPostReaction ); diff --git a/client/coral-framework/services/perms.js b/client/coral-framework/services/perms.js index e93744506..825c978ca 100644 --- a/client/coral-framework/services/perms.js +++ b/client/coral-framework/services/perms.js @@ -23,7 +23,13 @@ export const can = (user, ...perms) => { return false; } + const banned = user.status === 'BANNED'; + const suspended = user.suspension.until && new Date(user.suspension.until) > new Date(); + return perms.every((perm) => { + if (perm === 'INTERACT_WITH_COMMUNITY') { + return !banned && !suspended; + } const role = roles[perm]; if (typeof role === 'undefined') { throw new Error(`${perm} is not a valid role`); diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index 9a772f102..73ed50e9c 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -1,6 +1,7 @@ import React, {Component} from 'react'; import t from 'coral-framework/services/i18n'; +import {can} from 'coral-framework/services/perms'; import {PopupMenu, Button} from 'coral-ui'; import onClickOutside from 'react-onclickoutside'; @@ -32,10 +33,12 @@ class FlagButton extends Component { this.props.showSignInDialog(); return; } - if (this.state.showMenu) { - this.closeMenu(); - } else { - this.setState({showMenu: true}); + if (can(currentUser, 'INTERACT_WITH_COMMUNITY')) { + if (this.state.showMenu) { + this.closeMenu(); + } else { + this.setState({showMenu: true}); + } } } diff --git a/client/coral-plugin-replies/ReplyButton.js b/client/coral-plugin-replies/ReplyButton.js index 6fb6a5aca..a66e3c156 100644 --- a/client/coral-plugin-replies/ReplyButton.js +++ b/client/coral-plugin-replies/ReplyButton.js @@ -6,21 +6,20 @@ import classnames from 'classnames'; const name = 'coral-plugin-replies'; -const ReplyButton = ({banned, onClick}) => { +const ReplyButton = ({onClick}) => { return ( ); }; ReplyButton.propTypes = { onClick: PropTypes.func.isRequired, - banned: PropTypes.bool.isRequired }; export default ReplyButton; diff --git a/plugin-api/beta/client/services/index.js b/plugin-api/beta/client/services/index.js index 16872b9f3..509d4f55c 100644 --- a/plugin-api/beta/client/services/index.js +++ b/plugin-api/beta/client/services/index.js @@ -1 +1,2 @@ export {t} from 'coral-framework/services/i18n'; +export {can} from 'coral-framework/services/perms'; diff --git a/plugins/coral-plugin-auth/client/components/FakeComment.js b/plugins/coral-plugin-auth/client/components/FakeComment.js index 9900ccdc6..390dd409b 100644 --- a/plugins/coral-plugin-auth/client/components/FakeComment.js +++ b/plugins/coral-plugin-auth/client/components/FakeComment.js @@ -30,7 +30,6 @@ export const FakeComment = ({username, created_at, body}) => ( onClick={() => {}} parentCommentId={'commentID'} currentUserId={{}} - banned={false} />
    diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js index 4e9b56d44..de4c4c9e7 100644 --- a/plugins/coral-plugin-love/client/LoveButton.js +++ b/plugins/coral-plugin-love/client/LoveButton.js @@ -1,8 +1,8 @@ import React from 'react'; import {Icon} from 'coral-ui'; import styles from './styles.css'; -import t from 'coral-framework/services/i18n'; import {withReaction} from 'plugin-api/beta/client/hocs'; +import {t, can} from 'plugin-api/beta/client/services'; class LoveButton extends React.Component { handleClick = () => { @@ -10,18 +10,18 @@ class LoveButton extends React.Component { postReaction, deleteReaction, showSignInDialog, - alreadyReacted + alreadyReacted, + user, } = this.props; - const {root: {me}} = this.props; // If the current user does not exist, trigger sign in dialog. - if (!me) { + if (!user) { showSignInDialog(); return; } - // If the current user is banned, do nothing. - if (me.status === 'BANNED') { + // If the current user is suspended, do nothing. + if (!can(user, 'INTERACT_WITH_COMMUNITY')) { return; } From 38077cf3cfc19fdda0ce843fa9ab10fd72bf479e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 11:56:38 -0300 Subject: [PATCH 043/264] Missing fragment --- client/coral-embed-stream/src/graphql/index.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 7d20ea48b..aa6b5d9a2 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -36,18 +36,6 @@ const extension = { name } } - ModifyTagResponse: gql` - fragment CoralEmbedStream_ModifyTagResponse on ModifyTagResponse { - errors { - translation_key - } - } - `, - DeleteActionResponse: gql` - fragment CoralEmbedStream_DeleteActionResponse on DeleteActionResponse { - errors { - translation_key - } } `, CreateFlagResponse: gql` From 15b5dc2236fd52d9701df94a05049d028feb1f07 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 11:58:38 -0300 Subject: [PATCH 044/264] Linting --- client/coral-embed-stream/src/graphql/index.js | 2 +- plugins/coral-plugin-offtopic/index.js | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index aa6b5d9a2..7f62240b5 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -158,8 +158,8 @@ const extension = { assigned_by: { id: auth.toJS().user.id, }, + __typename: 'Tag' })), - tags: tags.map((t) => ({name: t, __typename: 'Tag'})), status: null, replyCount: 0, parent: parent_id diff --git a/plugins/coral-plugin-offtopic/index.js b/plugins/coral-plugin-offtopic/index.js index 9988686ff..eae6a2ef7 100644 --- a/plugins/coral-plugin-offtopic/index.js +++ b/plugins/coral-plugin-offtopic/index.js @@ -1,6 +1,3 @@ -const {readFileSync} = require('fs'); -const path = require('path'); - module.exports = { tags: [ { From 82a72e506fe9eb0542c26529e42d8fed329ba5be Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:04:33 -0300 Subject: [PATCH 045/264] Off-topic and Staff working --- client/coral-embed-stream/src/components/Comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index b5eca4ff2..d798b446c 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -26,8 +26,8 @@ import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import {getEditableUntilDate} from './util'; import styles from './Comment.css'; -const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF'); -const hasTag = (tags, lookupTag) => !!tags.filter((tag) => tag.name === lookupTag).length; +const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF'); +const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length; const hasComment = (nodes, id) => nodes.some((node) => node.id === id); // resetCursors will return the id cursors of the first and second newest comment in From 3a285d92a5cc889cb802abd3e43470d80ecc3710 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 09:10:53 -0600 Subject: [PATCH 046/264] Added more warnings --- bin/cli-migration | 17 +++++++++++++++-- plugins.default.json | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bin/cli-migration b/bin/cli-migration index 9c3e077b1..83a84e29d 100755 --- a/bin/cli-migration +++ b/bin/cli-migration @@ -41,7 +41,20 @@ async function runMigrations() { console.log(`\tmigrations/${filename}`); } - let answers = await inquirer.prompt([ + let {backedUp} = await inquirer.prompt([ + { + type: 'confirm', + name: 'backedUp', + message: 'Did you perform a database backup', + default: false + } + ]); + + if (!backedUp) { + throw new Error('Please backup your databases prior to migrations occuring'); + } + + let {confirm} = await inquirer.prompt([ { type: 'confirm', name: 'confirm', @@ -50,7 +63,7 @@ async function runMigrations() { } ]); - if (answers.confirm) { + if (confirm) { // Run the migrations. await MigrationService.run(migrations); diff --git a/plugins.default.json b/plugins.default.json index 753eb9000..2fb3aaa21 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -4,7 +4,7 @@ "coral-plugin-like", "coral-plugin-respect", "coral-plugin-offtopic", - "coral-plugin-facebook-auth", + "coral-plugin-facebook-auth" ], "client": [ "coral-plugin-respect", From 480bab3c1a170b5d882f39812f493b9ff03ee227 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 09:12:08 -0600 Subject: [PATCH 047/264] changed some text around --- bin/cli-migration | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/cli-migration b/bin/cli-migration index 83a84e29d..036ac8b02 100755 --- a/bin/cli-migration +++ b/bin/cli-migration @@ -32,15 +32,6 @@ async function runMigrations() { try { - // Get the migrations to run. - let migrations = await MigrationService.listPending(); - - console.log('Now going to run the following migrations:\n'); - - for (let {filename} of migrations) { - console.log(`\tmigrations/${filename}`); - } - let {backedUp} = await inquirer.prompt([ { type: 'confirm', @@ -54,6 +45,15 @@ async function runMigrations() { throw new Error('Please backup your databases prior to migrations occuring'); } + // Get the migrations to run. + let migrations = await MigrationService.listPending(); + + console.log('Now going to run the following migrations:\n'); + + for (let {filename} of migrations) { + console.log(`\tmigrations/${filename}`); + } + let {confirm} = await inquirer.prompt([ { type: 'confirm', From 1f261f5216f03f1adaa7f408b0e5945d15342a19 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:35:42 -0300 Subject: [PATCH 048/264] Fully working --- .../src/components/Comment.js | 22 ++++++++++--------- .../src/components/Stream.js | 12 +++++----- .../coral-embed-stream/src/graphql/index.js | 8 +++---- client/coral-framework/graphql/mutations.js | 16 ++++++++------ .../client/components/OffTopicTag.js | 5 +---- test/server/graph/mutations/removeTag.js | 2 +- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index d798b446c..4233325c5 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -180,10 +180,10 @@ export default class Comment extends React.Component { commentIsIgnored: React.PropTypes.func, // dispatch action to add a tag to a comment - addCommentTag: React.PropTypes.func, + addTag: React.PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: React.PropTypes.func, + removeTag: React.PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, @@ -286,11 +286,11 @@ export default class Comment extends React.Component { deleteAction, disableReply, maxCharCount, - addCommentTag, addNotification, charCountEnable, showSignInDialog, - removeCommentTag, + addTag, + removeTag, liveUpdates, commentIsIgnored, commentClassNames = [] @@ -333,18 +333,20 @@ export default class Comment extends React.Component { const addBestTag = notifyOnError( () => - addCommentTag({ + addTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + assetId: asset.id }), () => 'Failed to tag comment as best' ); const removeBestTag = notifyOnError( () => - removeCommentTag({ + removeTag({ id: comment.id, - tag: BEST_TAG + name: BEST_TAG, + assetId: asset.id }), () => 'Failed to remove best comment tag' ); @@ -547,8 +549,8 @@ export default class Comment extends React.Component { currentUser={currentUser} postFlag={postFlag} deleteAction={deleteAction} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} charCountEnable={charCountEnable} maxCharCount={maxCharCount} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 586d28f23..91e696652 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -129,10 +129,10 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - addCommentTag, + addTag, ignoreUser, auth: {loggedIn, user}, - removeCommentTag, + removeTag, pluginProps, editName } = this.props; @@ -264,8 +264,8 @@ class Stream extends React.Component { currentUser={user} postFlag={postFlag} postDontAgree={postDontAgree} - addCommentTag={addCommentTag} - removeCommentTag={removeCommentTag} + addTag={addTag} + removeTag={removeTag} ignoreUser={ignoreUser} commentIsIgnored={commentIsIgnored} loadMore={this.props.loadNewReplies} @@ -298,10 +298,10 @@ Stream.propTypes = { postComment: PropTypes.func.isRequired, // dispatch action to add a tag to a comment - addCommentTag: PropTypes.func, + addTag: PropTypes.func, // dispatch action to remove a tag from a comment - removeCommentTag: PropTypes.func, + removeTag: PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 7f62240b5..dab648c8b 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -18,8 +18,8 @@ const extension = { } } `, - RemoveCommentTagResponse: gql` - fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse { + RemoveTagResponse: gql` + fragment CoralEmbedStream_RemoveTagResponse on RemoveTagResponse { comment { id tags { @@ -28,8 +28,8 @@ const extension = { } } `, - AddCommentTagResponse: gql` - fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse { + AddTagResponse: gql` + fragment CoralEmbedStream_AddTagResponse on AddTagResponse { comment { id tags { diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 183dead09..6660af999 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -174,7 +174,7 @@ export const withDeleteAction = withMutation( }); const COMMENT_FRAGMENT = gql` - fragment CoraBest_UpdateFragment on Comment { + fragment CoralBest_UpdateFragment on Comment { tags { tag { name @@ -185,9 +185,11 @@ const COMMENT_FRAGMENT = gql` export const withAddTag = withMutation( gql` - mutation AddCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + mutation AddTag($id: ID!, $asset_id: ID!, $name: String!) { addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { - ...ModifyTagResponse + errors { + translation_key + } } } `, { @@ -228,7 +230,7 @@ export const withAddTag = withMutation( export const withRemoveTag = withMutation( gql` - mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) { removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key @@ -237,12 +239,12 @@ export const withRemoveTag = withMutation( } `, { props: ({mutate}) => ({ - removeTag: ({id, name, asset_id}) => { + removeTag: ({id, name, assetId}) => { return mutate({ variables: { id, name, - asset_id + asset_id: assetId }, update: (proxy) => { const fragmentId = `Comment_${id}`; @@ -250,7 +252,7 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - const idx = data.tags.findIndex((i) => i.tag.name === 'BEST'); + const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js index e8029ce5e..0e028c6bf 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js @@ -1,11 +1,8 @@ import React from 'react'; import styles from './styles.css'; - import {t} from 'plugin-api/beta/client/services'; -const isOffTopic = (tags) => { - return !!tags.filter((tag) => tag.name === 'OFF_TOPIC').length; -}; +const isOffTopic = (tags) => !!tags.filter((t) => t.tag.name === 'OFF_TOPIC').length; export default (props) => ( diff --git a/test/server/graph/mutations/removeTag.js b/test/server/graph/mutations/removeTag.js index 95b3e8bd2..a1521c2ce 100644 --- a/test/server/graph/mutations/removeTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -23,7 +23,7 @@ describe('graph.mutations.removeTag', () => { }); const query = ` - mutation RemoveCommentTag($id: ID!, $asset_id: ID!, $name: String!) { + mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) { removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { errors { translation_key From f6798af74d38f0b115e98dde1f3fcd59a0ac7577 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:37:37 -0300 Subject: [PATCH 049/264] Lint --- client/coral-framework/graphql/mutations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 6660af999..20ed175b3 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -252,7 +252,7 @@ export const withRemoveTag = withMutation( // Read the data from our cache for this query. const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId}); - const idx = data.tags.findIndex(i => i.tag.name === 'BEST'); + const idx = data.tags.findIndex((i) => i.tag.name === 'BEST'); data.tags = [...data.tags.slice(0, idx), ...data.tags.slice(idx + 1)]; From 283c13fae8551cf1906bd163d5bdfb2813155ad8 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 8 Jun 2017 12:43:02 -0300 Subject: [PATCH 050/264] =?UTF-8?q?=C3=81dding=20typenames=20to=20update?= =?UTF-8?q?=20the=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-embed-stream/src/graphql/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index dab648c8b..7ca22a5f4 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -154,11 +154,13 @@ const extension = { tag: { name: tag, created_at: new Date().toISOString(), + __typename: 'Tag' }, assigned_by: { id: auth.toJS().user.id, + __typename: 'User' }, - __typename: 'Tag' + __typename: 'TagLink' })), status: null, replyCount: 0, From 8eb8010a3346d19aedd19bd04031c5b208202d69 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 8 Jun 2017 23:54:08 +0700 Subject: [PATCH 051/264] Implement Response interface --- plugins/coral-plugin-like/server/typeDefs.graphql | 2 +- plugins/coral-plugin-love/server/typeDefs.graphql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/coral-plugin-like/server/typeDefs.graphql b/plugins/coral-plugin-like/server/typeDefs.graphql index 1a1100754..40c600f2f 100644 --- a/plugins/coral-plugin-like/server/typeDefs.graphql +++ b/plugins/coral-plugin-like/server/typeDefs.graphql @@ -60,7 +60,7 @@ type CreateLikeResponse implements Response { like: LikeAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } type RootMutation { diff --git a/plugins/coral-plugin-love/server/typeDefs.graphql b/plugins/coral-plugin-love/server/typeDefs.graphql index e22b91a2d..edc45e20b 100644 --- a/plugins/coral-plugin-love/server/typeDefs.graphql +++ b/plugins/coral-plugin-love/server/typeDefs.graphql @@ -60,7 +60,7 @@ type CreateLoveResponse implements Response { love: LoveAction # An array of errors relating to the mutation that occurred. - errors: [UserError] + errors: [UserError!] } type RootMutation { From 9b839b76f508390629a12a814b3c5ad87844d67f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 8 Jun 2017 23:55:37 +0700 Subject: [PATCH 052/264] Don't assume response name pattern --- client/coral-framework/graphql/fragments.js | 34 +++++++++------------ client/coral-framework/utils/index.js | 15 +++++++++ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js index 9c396cbaa..3c64090bd 100644 --- a/client/coral-framework/graphql/fragments.js +++ b/client/coral-framework/graphql/fragments.js @@ -1,23 +1,19 @@ -import {gql} from 'react-apollo'; -import * as mutations from './mutations'; - -function createDefaultResponseFragments() { - const names = Object.keys(mutations).map((key) => key.replace('with', '')); - const result = {}; - names.forEach((name) => { - const response = `${name}Response`; - result[response] = gql` - fragment Coral_${response} on ${response} { - errors { - translation_key - } - } - `; - }); - return result; -} +import {createDefaultResponseFragments} from '../utils'; // fragments defined here are automatically registered. export default { - ...createDefaultResponseFragments() + ...createDefaultResponseFragments( + 'SetCommentStatusResponse', + 'SuspendUserResponse', + 'RejectUsernameResponse', + 'SetUserStatusResponse', + 'PostCommentResponse', + 'EditCommentResponse', + 'PostFlagResponse', + 'CreateDontAgreeResponse', + 'DeleteActionResponse', + 'ModifyTagResponse', + 'IgnoreUserResponse', + 'StopIgnoringUserResponse', + ) }; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index ca2dd5ab2..1d430f1fc 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -112,3 +112,18 @@ export function getResponseErrors(mutationResult) { }); return result.length ? result : false; } + +export function createDefaultResponseFragments(...names) { + const result = {}; + names.forEach((name) => { + const response = `${name}Response`; + result[response] = gql` + fragment Coral_${response} on ${response} { + errors { + translation_key + } + } + `; + }); + return result; +} From 80900a98fdae57bb7d26f7eed1294b973dac1d4e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:19:43 -0600 Subject: [PATCH 053/264] perform migrations during web install --- bin/cli-serve | 42 +++++++++++++++++++++--- services/setup.js | 84 +++++++++++++++++++++++------------------------ 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/bin/cli-serve b/bin/cli-serve index 63eda3af2..f2a6d3592 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -2,10 +2,13 @@ const program = require('./commander'); const app = require('../app'); +const debug = require('debug')('talk:cli:serve'); +const errors = require('../errors'); const {createServer} = require('http'); const scraper = require('../services/scraper'); const mailer = require('../services/mailer'); const MigrationService = require('../services/migration'); +const SetupService = require('../services/setup'); const kue = require('../services/kue'); const mongoose = require('../services/mongoose'); const util = require('./util'); @@ -92,12 +95,41 @@ async function startApp(program) { try { - // Verify that the minimum migration version is met. - await MigrationService.verify(); + // Check to see if the application is installed. If the application + // has been installed, then it will throw errors.ErrSettingsNotInit, this + // just means we don't have to check that the migrations have run. + await SetupService.isAvailable(); - } catch(e) { - console.error(e); - process.exit(1); + debug('setup is currently available, migrations not being checked'); + + } catch (e) { + + // Check the error. + switch (e) { + case errors.ErrInstallLock, errors.ErrSettingsInit: + + debug('setup is not currently available, migrations now being checked'); + + // The error was expected, just continue. + break; + default: + + // The error was not expected, throw the error! + throw e; + } + + // Now try and check the migration status. + try { + + // Verify that the minimum migration version is met. + await MigrationService.verify(); + + } catch (e) { + console.error(e); + process.exit(1); + } + + debug('migrations do not have to be run'); } /** diff --git a/services/setup.js b/services/setup.js index 233021c17..053a85789 100644 --- a/services/setup.js +++ b/services/setup.js @@ -1,5 +1,6 @@ const UsersService = require('./users'); const SettingsService = require('./settings'); +const MigrationService = require('./migration'); const SettingsModel = require('../models/setting'); const errors = require('../errors'); const { @@ -15,34 +16,32 @@ module.exports = class SetupService { /** * This returns a promise which resolves if the setup is available. */ - static isAvailable() { + static async isAvailable() { // Check if we have an install lock present. if (INSTALL_LOCK) { - return Promise.reject(errors.ErrInstallLock); + throw errors.ErrInstallLock; } - // Get the current settings, we are expecing an error here. - return SettingsService - .retrieve() - .then(() => { + try { - // We should NOT have gotten a settings object, this means that the - // application is already setup. Error out here. - return Promise.reject(errors.ErrSettingsInit); + // Get the current settings, we are expecing an error here. + await SettingsService.retrieve(); + + // We should NOT have gotten a settings object, this means that the + // application is already setup. Error out here. + throw errors.ErrSettingsInit; + } catch (e) { - }) - .catch((err) => { + // If the error is `not init`, then we're good, otherwise, it's something + // else. + if (e !== errors.ErrSettingsNotInit) { + throw e; + } - // If the error is `not init`, then we're good, otherwise, it's something - // else. - if (err !== errors.ErrSettingsNotInit) { - return Promise.reject(err); - } - - // Allow the request to keep going here. - return; - }); + // Allow the request to keep going here. + return; + } } /** @@ -69,35 +68,34 @@ module.exports = class SetupService { /** * This will perform the setup. */ - static setup({settings, user: {email, password, username}}) { + static async setup({settings, user: {email, password, username}}) { // Validate the settings first. - return SetupService - .validate({settings, user: {email, password, username}}) - .then(() => { - return SettingsService.update(settings); - }) - .then((settings) => { + await SetupService.validate({settings, user: {email, password, username}}); - // Settings are created! Create the user. + // Get the migrations to run. + let migrations = await MigrationService.listPending(); - // Create the user. - return UsersService - .createLocalUser(email, password, username) + // Perform all migrations. + await MigrationService.run(migrations); - // Grant them administrative privileges and confirm the email account. - .then((user) => { + settings = await SettingsService.update(settings); - return Promise.all([ - UsersService.addRoleToUser(user.id, 'ADMIN'), - UsersService.confirmEmail(user.id, email) - ]) - .then(() => ({ - settings, - user - })); - }); - }); + // Settings are created! Create the user. + + // Create the user. + let user = await UsersService.createLocalUser(email, password, username); + + // Grant them administrative privileges and confirm the email account. + await Promise.all([ + UsersService.addRoleToUser(user.id, 'ADMIN'), + UsersService.confirmEmail(user.id, email) + ]); + + return { + settings, + user + }; } }; From b95e20e493f7356c0f1fdccbdad554fdfe5a5160 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:21:09 -0600 Subject: [PATCH 054/264] removed explicit migration step --- bin/cli-setup | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/bin/cli-setup b/bin/cli-setup index add134209..680fba91a 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -12,7 +12,6 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options'); const SettingsService = require('../services/settings'); const SetupService = require('../services/setup'); const UsersService = require('../services/users'); -const MigrationService = require('../services/migration'); const util = require('./util'); const errors = require('../errors'); @@ -64,37 +63,6 @@ const performSetup = async () => { } } - // Get the migrations to run. - let migrations = await MigrationService.listPending(); - if (migrations.length > 0) { - - console.log('Now going to run the following migrations:\n'); - - for (let {filename} of migrations) { - console.log(`\tmigrations/${filename}`); - } - - console.log(''); - - let {confirm} = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: 'Proceed with migrations', - default: false - } - ]); - - if (!confirm) { - throw new Error('migrations are needed to complete setup'); - } - - // Perform all migrations. - await MigrationService.run(migrations); - } else { - console.log('No migrations have to be run.'); - } - // Create the base settings model. let settings = new SettingModel(); From b06a58ebe1b96f9753fccc56b8b560914472e88a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:21:44 -0600 Subject: [PATCH 055/264] adjusted flow --- bin/cli-setup | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/cli-setup b/bin/cli-setup index 680fba91a..9ecdecc95 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -35,15 +35,6 @@ program const performSetup = async () => { - if (program.defaults) { - await SettingsService.init(); - - console.log('Settings created.'); - console.log('\nTalk is now installed!'); - - return; - } - // Get the current settings, we are expecing an error here. try { @@ -63,6 +54,15 @@ const performSetup = async () => { } } + if (program.defaults) { + await SettingsService.init(); + + console.log('Settings created.'); + console.log('\nTalk is now installed!'); + + return; + } + // Create the base settings model. let settings = new SettingModel(); From 57747e8f13486d1e9ed32a4cb8353a4e3ed25757 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 00:22:58 +0700 Subject: [PATCH 056/264] Make mutations extendable --- client/coral-framework/graphql/mutations.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 20ed175b3..dcba97891 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -187,9 +187,7 @@ export const withAddTag = withMutation( gql` mutation AddTag($id: ID!, $asset_id: ID!, $name: String!) { addTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { - errors { - translation_key - } + ...ModifyTagResponse } } `, { @@ -232,9 +230,7 @@ export const withRemoveTag = withMutation( gql` mutation RemoveTag($id: ID!, $asset_id: ID!, $name: String!) { removeTag(tag: {name: $name, id: $id, item_type: COMMENTS, asset_id: $asset_id}) { - errors { - translation_key - } + ...ModifyTagResponse } } `, { From 995dd9fd6065b15717a481ee5156ea0053ecff5c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 00:31:19 +0700 Subject: [PATCH 057/264] Correct fragment names --- client/coral-framework/utils/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 1d430f1fc..0b79b7b02 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -115,8 +115,7 @@ export function getResponseErrors(mutationResult) { export function createDefaultResponseFragments(...names) { const result = {}; - names.forEach((name) => { - const response = `${name}Response`; + names.forEach((response) => { result[response] = gql` fragment Coral_${response} on ${response} { errors { From 1153c52c2c702529df6d9d7209986a3b8f849eb8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 00:31:29 +0700 Subject: [PATCH 058/264] Correct optimistic response --- client/coral-framework/graphql/mutations.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index dcba97891..3b9ef01f4 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -200,8 +200,8 @@ export const withAddTag = withMutation( asset_id: assetId }, optimisticResponse: { - deleteAction: { - __typename: 'DeleteActionResponse', + addTag: { + __typename: 'ModifyTagResponse', errors: null, } }, @@ -242,6 +242,12 @@ export const withRemoveTag = withMutation( name, asset_id: assetId }, + optimisticResponse: { + removeTag: { + __typename: 'ModifyTagResponse', + errors: null, + } + }, update: (proxy) => { const fragmentId = `Comment_${id}`; From a9a24b96c0ae6cea62eb8210fa9109c53f7dd086 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:49:36 -0600 Subject: [PATCH 059/264] cleaned up plugins in webpack --- plugins.js | 10 +++++++--- webpack.config.js | 28 ++++++---------------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/plugins.js b/plugins.js index 4403f89bb..fa17027a2 100644 --- a/plugins.js +++ b/plugins.js @@ -10,6 +10,7 @@ const PLUGINS_JSON = process.env.TALK_PLUGINS_JSON; // Add the current path to the module root. amp.addPath(__dirname); +let pluginsPath; let plugins = {}; // Try to parse the plugins.json file, logging out an error if the plugins.json @@ -22,14 +23,16 @@ try { if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { debug('Now using TALK_PLUGINS_JSON environment variable for plugins'); - plugins = require(envPlugins); + pluginsPath = envPlugins; } else if (fs.existsSync(customPlugins)) { debug(`Now using ${customPlugins} for plugins`); - plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8')); + pluginsPath = customPlugins; } else { debug(`Now using ${defaultPlugins} for plugins`); - plugins = JSON.parse(fs.readFileSync(defaultPlugins, 'utf8')); + pluginsPath = defaultPlugins; } + + plugins = require(pluginsPath); } catch (err) { if (err.code === 'ENOENT') { console.error('plugins.json and plugins.default.json not found, plugins will not be active'); @@ -249,6 +252,7 @@ class PluginManager { module.exports = { plugins, + pluginsPath, PluginManager, isInternal, pluginPath, diff --git a/webpack.config.js b/webpack.config.js index d2cd07300..3973adc6d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,4 @@ const path = require('path'); -const fs = require('fs'); const CompressionPlugin = require('compression-webpack-plugin'); const autoprefixer = require('autoprefixer'); const precss = require('precss'); @@ -10,26 +9,11 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); -const {plugins, PluginManager} = require('./plugins'); +const {plugins, pluginsPath, PluginManager} = require('./plugins'); +const manager = new PluginManager(plugins); +const targetPlugins = manager.section('targets').plugins; -const pluginManager = new PluginManager(plugins); -const targetPlugins = pluginManager.section('targets').plugins; - -let pluginsConfigPath; - -let envPlugins = path.join(__dirname, 'plugins.env.js'); -let customPlugins = path.join(__dirname, 'plugins.json'); -let defaultPlugins = path.join(__dirname, 'plugins.default.json'); - -if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { - pluginsConfigPath = envPlugins; -} else if (fs.existsSync(customPlugins)) { - pluginsConfigPath = customPlugins; -} else { - pluginsConfigPath = defaultPlugins; -} - -console.log(`Using ${pluginsConfigPath} as the plugin configuration path`); +console.log(`Using ${pluginsPath} as the plugin configuration path`); // Edit the build targets and embeds below. @@ -89,7 +73,7 @@ const config = { { loader: 'plugins-loader', test: /\.(json|js)$/, - include: pluginsConfigPath + include: pluginsPath }, { loader: 'babel-loader', @@ -168,7 +152,7 @@ const config = { resolve: { alias: { plugins: path.resolve(__dirname, 'plugins/'), - pluginsConfig: pluginsConfigPath + pluginsConfig: pluginsPath }, modules: [ path.resolve(__dirname, 'plugins'), From 428246cbe2f199246e3e2bbe9d1b39afef95e68e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 01:16:39 +0700 Subject: [PATCH 060/264] Make it work without iframe --- client/coral-embed-stream/src/index.js | 27 ++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 821399488..16c510c29 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -19,14 +19,29 @@ loadPluginsTranslations(); injectPluginsReducers(); injectReducers(reducers); +function inIframe() { + try { + return window.self !== window.top; + } catch (e) { + return true; + } +} + +function init(config = {}) { + store.dispatch(addExternalConfig(config)); + store.dispatch(checkLogin()); +} + // Don't run this in the popup. if (!window.opener) { - pym.sendMessage('getConfig'); - - pym.onMessage('config', (config) => { - store.dispatch(addExternalConfig(JSON.parse(config))); - store.dispatch(checkLogin()); - }); + if (inIframe()) { + pym.sendMessage('getConfig'); + pym.onMessage('config', (config) => { + init(JSON.parse(config)); + }); + } else { + init(); + } } render( From 4e5680577e1f086327b0087f86d0621e9688c82b Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 01:17:27 +0700 Subject: [PATCH 061/264] Include public comments --- graph/resolvers/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index 9dd67bb0e..ef3c70865 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -9,7 +9,7 @@ const decorateWithTags = (typeResolver) => { return tags; } - return tags.filter((tag) => tag.public); + return tags.filter((t) => t.tag.permissions.public); }; }; From dbb0a68766cd8a722469a555569db67570eb82c5 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 8 Jun 2017 15:53:12 -0400 Subject: [PATCH 062/264] Prep to release 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d78ba8713..bb804c6e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "1.9.1", + "version": "2.0.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "scripts": { From d0b3845d0f92412532647beed166672c7c6d814f Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Thu, 8 Jun 2017 15:55:41 -0400 Subject: [PATCH 063/264] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 48055e326..c7ff21cdd 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ sign and verify tokens via a `HS256` algorithm. Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for alternative methods of loading configuration during development. +### Running Migrations + +We have a migration tool that can be run using `bin/cli-migration run`. This will detect new migrations available and prompt you to backup your database before proceeding with the migration. Migrations are required with major version releases. + ### Using Trust Talk ships with core components we call "Trust". This allows Talk to automate certain actions based on previous user behavior. From 32708e9098e9c32c69c484d8d77afb435880862e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 15:08:18 -0600 Subject: [PATCH 064/264] submscription fixes --- client/coral-framework/actions/auth.js | 11 ++++++ client/coral-framework/services/client.js | 42 +++++++++++++++++++++-- graph/subscriptions.js | 28 ++++++++++++--- services/subscriptions.js | 32 ++++++----------- 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 91f709b35..fe6546296 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -4,6 +4,7 @@ import * as actions from '../constants/auth'; import * as Storage from '../helpers/storage'; import coralApi, {base} from '../helpers/request'; +import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; export const showSignInDialog = () => (dispatch, getState) => { @@ -117,6 +118,12 @@ const signInFailure = (error) => ({ export const handleAuthToken = (token) => (dispatch) => { Storage.setItem('exp', jwtDecode(token).exp); Storage.setItem('token', token); + + alert('handled the auth token!'); + + // Reset the websocket. + resetWebsocket(); + dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; @@ -277,6 +284,10 @@ export const logout = () => (dispatch) => { if (!bowser.safari && !bowser.ios) { Storage.removeItem('token'); } + + // Reset the websocket. + resetWebsocket(); + dispatch({type: actions.LOGOUT}); }); }; diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index 6095e17ab..70408248c 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -1,8 +1,31 @@ import ApolloClient, {addTypename} from 'apollo-client'; import {networkInterface} from './transport'; import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws'; +import {SUBSCRIPTION_END} from 'subscriptions-transport-ws/dist/messageTypes'; +import {getAuthToken} from '../helpers/request'; -let client; +let client, wsClient = null, wsClientToken = null; + +export function resetWebsocket() { + if (wsClient === null) { + + // Nothing to reset! + return; + } + + // Unsubscribe from all the active subscriptions. + Object.keys(wsClient.subscriptions).forEach((id) => { + + // Create the message. + let message = {id: parseInt(id), type: SUBSCRIPTION_END}; + + // Send the unsubscribe message. + wsClient.client.send(JSON.stringify(message)); + }); + + // Close the client, this will trigger a reconnect. + wsClient.close(); +} export function getClient() { if (client) { @@ -10,8 +33,21 @@ export function getClient() { } const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; - const wsClient = new SubscriptionClient(`${protocol}://${location.host}/api/v1/live`, { - reconnect: true + wsClient = new SubscriptionClient(`${protocol}://${location.host}/api/v1/live`, { + reconnect: true, + connectionParams: { + get token() { + + wsClientToken = getAuthToken(); + + // Try to get the token from localStorage. If it isn't here, it may + // be passed as a cookie. + + // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT + // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. + return wsClientToken; + } + } }); const networkInterfaceWithSubscriptions = addGraphQLSubscriptions( diff --git a/graph/subscriptions.js b/graph/subscriptions.js index 0442c237d..0cc37e841 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -41,16 +41,34 @@ const createSubscriptionManager = (server) => new SubscriptionServer({ pubsub, setupFunctions, }), + onConnect: ({token}, connection) => { + + // Attach the token from the connection options if it was provided. + if (token) { + + // Attach it to the upgrade request. + connection.upgradeReq.headers['authorization'] = `Bearer ${token}`; + } + }, onSubscribe: (parsedMessage, baseParams, connection) => { + // Cache the upgrade request. + let upgradeReq = connection.upgradeReq; + // Attach the context per request. - baseParams.context = () => deserializeUser(connection.upgradeReq) - .then((req) => new Context(req, pubsub)) - .catch((err) => { - console.error(err); + baseParams.context = async () => { + let req; + + try { + req = await deserializeUser(upgradeReq); + } catch (e) { + console.error(e); return new Context({}, pubsub); - }); + } + + return new Context(req, pubsub); + }; return baseParams; } diff --git a/services/subscriptions.js b/services/subscriptions.js index c4cd2378a..d6a5b430c 100644 --- a/services/subscriptions.js +++ b/services/subscriptions.js @@ -1,4 +1,4 @@ -const passport = require('./passport'); +const cookieParser = require('cookie-parser')(); const authentication = require('../middleware/authentication'); // Session data does not automatically attach to websocket req objects. @@ -7,32 +7,22 @@ const authentication = require('../middleware/authentication'); const deserializeUser = (req) => { return new Promise((resolve, reject) => { - // This uses the authentication connect middleware to establish the session - // user details from the headers. - authentication(req, null, (err) => { + // Parse the cookies (if they exist?). + cookieParser(req, null, (err) => { if (err) { return reject(err); } - if ('session' in req && 'passport' in req.session && 'user' in req.session.passport) { - passport.deserializeUser(req.session.passport.user, (err, user) => { - if (err) { - return reject(err); - } - - req.user = user; - - return resolve(req); - }); - } - - // Remove the user from the request (if there was one) - if (req.user) { - delete req.user; - } + // This uses the authentication connect middleware to establish the + // current user. + authentication(req, null, (err) => { + if (err) { + return reject(err); + } // Resolve with the request (user removed possibly). - return resolve(req); + return resolve(req); + }); }); }); }; From 399f1e74584fd1ddaa84a7a13dee075e8aa83d01 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 16:53:22 -0600 Subject: [PATCH 065/264] simplified webpack file --- plugins.js | 1 + webpack.config.js | 100 ++++++++++++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/plugins.js b/plugins.js index fa17027a2..dfe3d3312 100644 --- a/plugins.js +++ b/plugins.js @@ -84,6 +84,7 @@ function pluginPath(name) { try { return resolve.sync(name, {moduleDirectory: 'plugins', basedir: process.cwd()}); } catch (e) { + console.warn(e); return undefined; } } diff --git a/webpack.config.js b/webpack.config.js index 3973adc6d..c580a0c13 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,7 @@ const path = require('path'); const CompressionPlugin = require('compression-webpack-plugin'); const autoprefixer = require('autoprefixer'); const precss = require('precss'); +const _ = require('lodash'); const Copy = require('copy-webpack-plugin'); const LicenseWebpackPlugin = require('license-webpack-plugin'); const webpack = require('webpack'); @@ -15,8 +16,6 @@ const targetPlugins = manager.section('targets').plugins; console.log(`Using ${pluginsPath} as the plugin configuration path`); -// Edit the build targets and embeds below. - const buildTargets = [ 'coral-admin', 'coral-docs' @@ -26,47 +25,16 @@ const buildEmbeds = [ 'stream' ]; +//============================================================================== +// Base Webpack Config +//============================================================================== + const config = { devtool: 'cheap-module-source-map', - entry: Object.assign({}, { - 'embed': [ - 'babel-polyfill', - path.join(__dirname, 'client/coral-embed/src/index') - ] - }, buildTargets.reduce((entry, target) => { - - // Add the entry for the bundle. - entry[`${target}/bundle`] = [ - 'babel-polyfill', - path.join(__dirname, 'client/', target, '/src/index') - ]; - - return entry; - }, {}), buildEmbeds.reduce((entry, embed) => { - - // Add the entry for the bundle. - entry[`embed/${embed}/bundle`] = [ - 'babel-polyfill', - path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') - ]; - - return entry; - }, {}), targetPlugins.reduce((entry, plugin) => { - entry[`${plugin.name}/bundle`] = [ - 'babel-polyfill', - plugin.path - ]; - - return entry; - }, {})), output: { path: path.join(__dirname, 'dist'), publicPath: '/client/', - filename: '[name].js', - - // NOTE: this causes all exports to override the global.Coral, so no more - // than one bundle.js can be included on a page. - library: 'Coral' + filename: '[name].js' }, module: { rules: [ @@ -164,6 +132,10 @@ const config = { } }; +//============================================================================== +// Production configuration overrides +//============================================================================== + if (process.env.NODE_ENV === 'production') { config.plugins.push(new CompressionPlugin({ asset: '[path].gz[query]', @@ -174,4 +146,54 @@ if (process.env.NODE_ENV === 'production') { })); } -module.exports = config; +//============================================================================== +// Entries +//============================================================================== + +// Applies the base configuration to the following entries. +const applyConfig = (entries, root = {}) => _.merge({}, config, { + entry: entries.reduce((entry, {name, path}) => { + entry[name] = [ + 'babel-polyfill', + path + ]; + + return entry; + }, {}) +}, root); + +module.exports = [ + applyConfig([ + + // Load in the root embed. + { + name: 'embed', + path: path.join(__dirname, 'client/coral-embed/src/index') + } + + ], { + output: { + library: 'Coral' + } + }), + applyConfig([ + + // Load in all the targets. + ...buildTargets.map((target) => ({ + name: `${target}/bundle`, + path: path.join(__dirname, 'client/', target, '/src/index') + })), + + // Load in all the embeds. + ...buildEmbeds.map((embed) => ({ + name: `embed/${embed}/bundle`, + path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + })), + + // Load in all the plugin entries. + ...targetPlugins.map(({name, path}) => ({ + name: `${name}/bundle`, + path + })) + ]) +]; From 8d2c1b72472e3d0d3db5c02182a53fd60d1ab99e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 17:25:27 -0600 Subject: [PATCH 066/264] changed bundle format --- webpack.config.js | 88 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index c580a0c13..59fe9607a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const fs = require('fs'); const CompressionPlugin = require('compression-webpack-plugin'); const autoprefixer = require('autoprefixer'); const precss = require('precss'); @@ -163,37 +164,70 @@ const applyConfig = (entries, root = {}) => _.merge({}, config, { }, root); module.exports = [ + + // Coral Embed + // applyConfig([ + + // // Load in the root embed. + // { + // name: 'embed', + // path: path.join(__dirname, 'client/coral-embed/src/index') + // } + + // ], { + // output: { + // library: 'Coral' + // } + // }), + + // All framework targets/embeds/plugins. applyConfig([ - // Load in the root embed. - { - name: 'embed', - path: path.join(__dirname, 'client/coral-embed/src/index') - } + // // Load in all the targets. + // ...buildTargets.map((target) => ({ + // name: `${target}/bundle`, + // path: path.join(__dirname, 'client/', target, '/src/index') + // })), - ], { - output: { - library: 'Coral' - } - }), - applyConfig([ - - // Load in all the targets. - ...buildTargets.map((target) => ({ - name: `${target}/bundle`, - path: path.join(__dirname, 'client/', target, '/src/index') - })), - - // Load in all the embeds. - ...buildEmbeds.map((embed) => ({ - name: `embed/${embed}/bundle`, - path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') - })), + // // Load in all the embeds. + // ...buildEmbeds.map((embed) => ({ + // name: `embed/${embed}/bundle`, + // path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + // })), // Load in all the plugin entries. - ...targetPlugins.map(({name, path}) => ({ - name: `${name}/bundle`, - path - })) + ...targetPlugins.reduce((entries, plugin) => { + + // Introspect the path to find a targets folder. + let folder = path.dirname(plugin.path); + let files = fs.readdirSync(folder); + + // While the folder does not contain the targets folder... + while (!files.includes('targets')) { + + // Try to go up a folder. + folder = path.normalize(path.join(folder, '..')); + + // And as long as we haven't gone too high + if (!(folder.includes(path.join(__dirname, 'node_modules')) || !folder.includes(path.join(__dirname, 'plugins')))) { + throw new Error(`target plugin ${plugin.name} does not have a 'targets' folder`); + } + + files = fs.readdirSync(folder); + } + + // List all targets available in that folder. + folder = path.join(folder, 'targets'); + + let targets = fs.readdirSync(folder); + if (targets.length === 0) { + throw new Error(`target plugin ${plugin.name} has no targets in it's target folder ${folder}`); + } + + return entries.concat(targets.map((target) => ({ + name: `plugin/${plugin.name}/${target}/bundle`, + path: path.join(folder, target, 'index') + }))); + }, []) ]) ]; From 99174565faa5d0c797bc30242909622e3fc6f31b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 17:32:01 -0600 Subject: [PATCH 067/264] fixes to webpack config (uncommenting) --- webpack.config.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 59fe9607a..94ecad27a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -166,34 +166,34 @@ const applyConfig = (entries, root = {}) => _.merge({}, config, { module.exports = [ // Coral Embed - // applyConfig([ + applyConfig([ - // // Load in the root embed. - // { - // name: 'embed', - // path: path.join(__dirname, 'client/coral-embed/src/index') - // } + // Load in the root embed. + { + name: 'embed', + path: path.join(__dirname, 'client/coral-embed/src/index') + } - // ], { - // output: { - // library: 'Coral' - // } - // }), + ], { + output: { + library: 'Coral' + } + }), // All framework targets/embeds/plugins. applyConfig([ // // Load in all the targets. - // ...buildTargets.map((target) => ({ - // name: `${target}/bundle`, - // path: path.join(__dirname, 'client/', target, '/src/index') - // })), + ...buildTargets.map((target) => ({ + name: `${target}/bundle`, + path: path.join(__dirname, 'client/', target, '/src/index') + })), - // // Load in all the embeds. - // ...buildEmbeds.map((embed) => ({ - // name: `embed/${embed}/bundle`, - // path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') - // })), + // Load in all the embeds. + ...buildEmbeds.map((embed) => ({ + name: `embed/${embed}/bundle`, + path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + })), // Load in all the plugin entries. ...targetPlugins.reduce((entries, plugin) => { From 1d3cc906eb61115b709e84e7161046ecdfebd89f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 17:34:01 -0600 Subject: [PATCH 068/264] small fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7ff21cdd..70dfc61c4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ alternative methods of loading configuration during development. ### Running Migrations -We have a migration tool that can be run using `bin/cli-migration run`. This will detect new migrations available and prompt you to backup your database before proceeding with the migration. Migrations are required with major version releases. +We have a migration tool that can be run using `bin/cli migration run`. This will detect new migrations available and prompt you to backup your database before proceeding with the migration. Migrations are required with major version releases. ### Using Trust From c7ca3c5e4a33100d87fd0e6f24bd72b8337aefb4 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 17:26:42 +0700 Subject: [PATCH 069/264] Reset websocket after successful login --- client/coral-framework/actions/auth.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index fe6546296..e212af99b 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -119,11 +119,6 @@ export const handleAuthToken = (token) => (dispatch) => { Storage.setItem('exp', jwtDecode(token).exp); Storage.setItem('token', token); - alert('handled the auth token!'); - - // Reset the websocket. - resetWebsocket(); - dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; @@ -318,6 +313,9 @@ export const checkLogin = () => (dispatch) => { dispatch(checkLoginSuccess(result.user)); + // Reset the websocket. + resetWebsocket(); + // Display create username dialog if necessary. if (result.user.canEditName && result.user.status !== 'BANNED') { dispatch(showCreateUsernameDialog()); From f929059daf38d6b9539b26abd23a94659cd1b283 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 9 Jun 2017 13:23:16 -0600 Subject: [PATCH 070/264] added asset merging + url updates --- bin/cli-assets | 156 +++++++++++++++++++++++---------- errors.js | 10 ++- services/assets.js | 59 ++++++++++++- test/server/services/assets.js | 94 ++++++++++++++++++-- 4 files changed, 261 insertions(+), 58 deletions(-) diff --git a/bin/cli-assets b/bin/cli-assets index 97b5b043c..4805bead0 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -8,9 +8,12 @@ const program = require('./commander'); const parseDuration = require('ms'); const Table = require('cli-table'); const AssetModel = require('../models/asset'); +const CommentModel = require('../models/comment'); +const AssetsService = require('../services/assets'); const mongoose = require('../services/mongoose'); const scraper = require('../services/scraper'); const util = require('./util'); +const inquirer = require('inquirer'); // Register the shutdown criteria. util.onshutdown([ @@ -20,65 +23,112 @@ util.onshutdown([ /** * Lists all the assets registered in the database. */ -function listAssets() { - AssetModel - .find({}) - .sort({'created_at': 1}) - .then((asset) => { - let table = new Table({ - head: [ - 'ID', - 'Title', - 'URL' - ] - }); +async function listAssets() { + try { + let assets = await AssetModel.find({}).sort({'created_at': 1}); - asset.forEach((asset) => { - table.push([ - asset.id, - asset.title ? asset.title : '', - asset.url ? asset.url : '' - ]); - }); - - console.log(table.toString()); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); + let table = new Table({ + head: [ + 'ID', + 'Title', + 'URL' + ] }); + + assets.forEach((asset) => { + table.push([ + asset.id, + asset.title ? asset.title : '', + asset.url ? asset.url : '' + ]); + }); + + console.log(table.toString()); + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } } -function refreshAssets(ageString) { - const now = new Date().getTime(); - const ageMs = parseDuration(ageString); - const age = new Date(now - ageMs); +async function refreshAssets(ageString) { + try { + const now = new Date().getTime(); + const ageMs = parseDuration(ageString); + const age = new Date(now - ageMs); - AssetModel.find({ - $or: [ - { - scraped: { - $lte: age + let assets = await AssetModel.find({ + $or: [ + { + scraped: { + $lte: age + } + }, + { + scraped: null } - }, - { - scraped: null - } - ] - }) + ] + }); - // Queue all the assets for scraping. - .then((assets) => Promise.all(assets.map(scraper.create))) + // Queue all the assets for scraping. + await Promise.all(assets.map(scraper.create)); - .then(() => { console.log('Assets were queued to be scraped'); util.shutdown(); - }) - .catch((err) => { - console.error(err); + } catch (e) { + console.error(e); util.shutdown(1); - }); + } +} + +async function updateURL(assetID, assetURL) { + try { + await AssetsService.updateURL(assetID, assetURL); + + console.log(`Asset ${assetID} was updated to have url ${assetURL}.`); + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } +} + +async function merge(srcID, dstID) { + try { + + // Grab the assets... + let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]); + if (!srcAsset || !dstAsset) { + throw new Error('Not all assets indicated by id exist, cannot merge'); + } + + // Count the affected resources... + let srcCommentCount = await CommentModel.find({asset_id: srcID}).count(); + + console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`); + + let {confirm} = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Proceed with merge', + default: false + } + ]); + + if (confirm) { + + // Perform the merge! + await AssetsService.merge(srcID, dstID); + } else { + console.warn('Aborting merge'); + } + + util.shutdown(0); + } catch (e) { + console.error(e); + util.shutdown(1); + } } //============================================================================== @@ -95,6 +145,16 @@ program .description('queues the assets that exceed the age requested') .action(refreshAssets); +program + .command('update-url ') + .description('update the URL of an asset') + .action(updateURL); + +program + .command('merge ') + .description('merges two assets together by moving comments from src to dst and deleting the src asset') + .action(merge); + program.parse(process.argv); // If there is no command listed, output help. diff --git a/errors.js b/errors.js index c203b1d25..87c3a2a65 100644 --- a/errors.js +++ b/errors.js @@ -168,6 +168,13 @@ const ErrCommentTooShort = new APIError('Comment was too short', { status: 400 }); +// ErrAssetURLAlreadyExists is returned when a rename operation is requested +// but an asset already exists with the new url. +const ErrAssetURLAlreadyExists = new APIError('Asset URL already exists, cannot rename', { + translation_key: 'ASSET_URL_ALREADY_EXISTS', + status: 409 +}); + module.exports = { ExtendableError, APIError, @@ -191,5 +198,6 @@ module.exports = { ErrInstallLock, ErrLoginAttemptMaximumExceeded, ErrEditWindowHasEnded, - ErrCommentTooShort + ErrCommentTooShort, + ErrAssetURLAlreadyExists }; diff --git a/services/assets.js b/services/assets.js index a86c4281f..b81513b72 100644 --- a/services/assets.js +++ b/services/assets.js @@ -1,3 +1,4 @@ +const CommentModel = require('../models/comment'); const AssetModel = require('../models/asset'); const SettingsService = require('./settings'); const domainlist = require('./domainlist'); @@ -128,9 +129,61 @@ module.exports = class AssetsService { * @param {Array} ids an array of Strings of asset_id * @return {Promise} resolves to list of Assets */ - static findMultipleById(ids) { - const query = ids.map((id) => ({id})); - return AssetModel.find(query); + static async findByIDs(ids) { + + // Find the assets. + let assets = await AssetModel.find({ + id: { + $in: ids + } + }); + + // Return them in the right order. + return ids.map((id) => assets.find((asset) => asset.id === id)); + } + + static async updateURL(id, url) { + + // Try to see if an asset already exists with the given url. + let asset = await AssetsService.findByUrl(url); + if (asset !== null) { + throw errors.ErrAssetURLAlreadyExists; + } + + // Seems that there was no other asset with the same url, try and perform + // the rename operation! An error may be thrown from this if the operation + // fails. This is ok. + await AssetModel.update({id}, {$set: {url}}); + } + + static async merge(srcAssetID, dstAssetID) { + + // Fetch both assets. + let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcAssetID, dstAssetID]); + if (!srcAsset || !dstAsset) { + throw errors.ErrNotFound; + } + + // Resolve the merge operation, this invloves moving all resources attached + // to the src asset to the dst asset, and then removing the src asset. + + // First, update all the old comments to the new asset. + await CommentModel.update({ + asset_id: srcAssetID + }, { + $set: { + asset_id: dstAssetID + } + }, { + multi: true + }); + + // Second remove the old asset. + await AssetModel.remove({ + id: srcAssetID + }); + + // That's it! } static all(skip = null, limit = null) { diff --git a/test/server/services/assets.js b/test/server/services/assets.js index e46e75576..748e177e0 100644 --- a/test/server/services/assets.js +++ b/test/server/services/assets.js @@ -1,22 +1,29 @@ const AssetModel = require('../../../models/asset'); +const CommentModel = require('../../../models/comment'); const AssetsService = require('../../../services/assets'); +const CommentsService = require('../../../services/comments'); const SettingsService = require('../../../services/settings'); +const url = require('url'); const chai = require('chai'); const expect = chai.expect; +const chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); // Use the chai should. chai.should(); +const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}}; +const defaults = {url:'http://test.com'}; + describe('services.AssetsService', () => { - beforeEach(() => { - const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}}; - const defaults = {url:'http://test.com'}; + let asset; + beforeEach(async () => { + await SettingsService.init(settings); - return SettingsService.init(settings).then(() => { - return AssetModel.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true}); - }); + asset = await AssetModel.findOneAndUpdate({id: '1'}, {$setOnInsert: defaults}, {upsert: true, new: true}); }); describe('#findById', ()=> { @@ -120,4 +127,79 @@ describe('services.AssetsService', () => { }); }); }); + + describe('#updateURL', () => { + + it('should change the url if the asset was found, and there was no conflict', async () => { + let newURL = url.resolve(asset.url, '/new-url'); + + // Update the asset. + await AssetsService.updateURL(asset.id, newURL); + + // Check that the url was updated. + let {url: databaseURL} = await AssetsService.findById(asset.id); + + expect(databaseURL).to.equal(newURL); + }); + + it('should error if the new url already exists', async () => { + let newURL = url.resolve(asset.url, '/new-url'); + + // Create a new asset with our new URL. + await AssetModel.findOneAndUpdate({id: '2'}, {$setOnInsert: {url: newURL}}, {upsert: true, new: true}); + + return AssetsService.updateURL(asset.id, newURL).should.eventually.be.rejected; + }); + + }); + + describe('#merge', () => { + + it('should error if either the src or the dst is missing', () => { + return AssetsService.merge('not-found', asset.id).should.eventually.be.rejected; + }); + + it('should merge the assets', async () => { + let newURL = url.resolve(asset.url, '/new-url'); + + // Create a new asset with our new URL. + await AssetModel.findOneAndUpdate({id: '2'}, {$setOnInsert: {url: newURL}}, {upsert: true, new: true}); + + // Create some comments on both assets. + await CommentsService.publicCreate([ + { + asset_id: '1', + body: 'This is a comment!', + status: 'ACCEPTED' + }, + { + asset_id: '1', + body: 'This is a comment!', + status: 'ACCEPTED' + }, + { + asset_id: '2', + body: 'This is a comment!', + status: 'ACCEPTED' + }, + { + asset_id: '2', + body: 'This is a comment!', + status: 'ACCEPTED' + } + ]); + + // Merge all the comments from asset 1 into asset 2, followed by deleting + // asset 1. + await AssetsService.merge('1', '2'); + + // Check to see if the comments are moved. + expect(await CommentModel.find({asset_id: '1'}).count()).to.equal(0); + expect(await CommentModel.find({asset_id: '2'}).count()).to.equal(4); + + // Check to see if the asset was removed. + expect(await AssetModel.findOne({id: '1'})).to.equal(null); + }); + + }); }); From de665b3a0d85b8ecce41e9ef7e9f6fc0e4d106b4 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 9 Jun 2017 14:10:21 -0600 Subject: [PATCH 071/264] show stories in dropdown --- client/coral-admin/src/actions/moderation.js | 2 + .../coral-admin/src/constants/moderation.js | 1 + client/coral-admin/src/reducers/moderation.js | 2 + .../Moderation/components/Moderation.js | 2 + .../src/routes/Moderation/components/Story.js | 22 ++++++ .../Moderation/components/StorySearch.css | 69 +++++++++++++++++++ .../Moderation/components/StorySearch.js | 47 +++++++++++++ .../Moderation/containers/Moderation.js | 2 +- .../Moderation/containers/StorySearch.js | 60 ++++++++++++++++ graph/loaders/assets.js | 12 ++++ graph/resolvers/root_query.js | 6 +- graph/typeDefs.graphql | 12 +++- 12 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 client/coral-admin/src/routes/Moderation/components/Story.js create mode 100644 client/coral-admin/src/routes/Moderation/components/StorySearch.css create mode 100644 client/coral-admin/src/routes/Moderation/components/StorySearch.js create mode 100644 client/coral-admin/src/routes/Moderation/containers/StorySearch.js diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 9f1c40c94..0833c856e 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -53,3 +53,5 @@ export const toggleSelectCommentInUserDetail = (id, active) => { }; export const toggleStorySearch = (active) => ({type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH}); + +export const storySearchChange = (e) => ({type: actions.CHANGE_ASSET_SEARCH_STRING, value: e.target.value}); diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index 29cf03143..37f048e23 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -14,3 +14,4 @@ export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT'; export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS'; export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH'; export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH'; +export const CHANGE_ASSET_SEARCH_STRING = 'CHANGE_ASSET_SEARCH_STRING'; diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index 4bab9eda1..349ff517a 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -86,6 +86,8 @@ export default function moderation (state = initialState, action) { return state.set('storySearchVisible', true); case actions.HIDE_STORY_SEARCH: return state.set('storySearchVisible', false); + case actions.CHANGE_ASSET_SEARCH_STRING: + return state.set('storySearchString', action.value); case actions.SET_SORT_ORDER: return state.set('sortOrder', action.order); default : diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index f7338cf6b..7582d10d1 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -10,6 +10,7 @@ import ModerationHeader from './ModerationHeader'; import NotFoundAsset from './NotFoundAsset'; import ModerationKeysModal from '../../../components/ModerationKeysModal'; import UserDetail from '../containers/UserDetail'; +import StorySearch from '../containers/StorySearch'; export default class Moderation extends Component { state = { @@ -209,6 +210,7 @@ export default class Moderation extends Component { acceptComment={props.acceptComment} rejectComment={props.rejectComment} /> )} +
    ); } diff --git a/client/coral-admin/src/routes/Moderation/components/Story.js b/client/coral-admin/src/routes/Moderation/components/Story.js new file mode 100644 index 000000000..ee9d462d3 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/components/Story.js @@ -0,0 +1,22 @@ +import React, {PropTypes} from 'react'; +import styles from './StorySearch.css'; + +const Story = ({author, title, createdAt, open}) => { + return ( +
  • +

    {title}

    +

    + By {author}{createdAt}{open ? 'Open' : 'Closed'} +

    +
  • + ); +}; + +Story.propTypes = { + author: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + createdAt: PropTypes.string.isRequired, + open: PropTypes.bool.isRequired +}; + +export default Story; diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.css b/client/coral-admin/src/routes/Moderation/components/StorySearch.css new file mode 100644 index 000000000..e5b6e4a93 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.css @@ -0,0 +1,69 @@ +.container { + position: fixed; + background-color: white; + top: 100px; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + width: 50%; + box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); +} + +.positionShim { + left: -50%; +} + +.headInput { + background-color: #efefef; + padding: 10px 30px; +} + +.searchInput { + width: calc(100% - 100px); + padding: 8px; + height: 100%; + font-size: 16px; + margin-right: 10px; + position: relative; + top: 2px; +} + +.cta { + padding: 15px; + font-weight: bold; + font-size: 14px; + margin: 0; +} + +.storyList { + padding: 10px; + margin: 0; + list-style: none; + border-top: 1px solid #ddd; +} + +.story { + padding: 10px; + border-bottom: 1px solid #ddd; +} + +.title, .meta { + margin: 0; +} + +.author, .createdAt, .status { + display: inline-block; + font-size: .8em; + color: #aaa; +} + +.author { + display: inline-block; + width: 200px; +} + +.createdAt { + display: inline-block; + width: 200px; +} diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.js b/client/coral-admin/src/routes/Moderation/components/StorySearch.js new file mode 100644 index 000000000..ce16e5b15 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.js @@ -0,0 +1,47 @@ +import React, {PropTypes} from 'react'; +import styles from './StorySearch.css'; +import {Button, Spinner} from 'coral-ui'; +import Story from './Story'; + +const StorySearch = (props) => { + + const { + root: {assets = []}, + data: {loading} + } = props; + + return ( +
    +
    +
    + + +
    +
    +

    Moderate comments on All Stories

    +
      + { + loading + ? + : assets.map((story, i) => { + const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); + return ; + }) + } +
    +
    +
    +
    + ); +}; + +StorySearch.propTypes = { + storySearchChange: PropTypes.func.isRequired +}; + +export default StorySearch; diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 31ac05450..24a3d748d 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -209,7 +209,7 @@ const withModQueueQuery = withQuery(gql` }) { ...CoralAdmin_Moderation_CommentConnection } - assets: assets { + assets: assets(query: {}) { id title url diff --git a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js new file mode 100644 index 000000000..cfe2a5bea --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js @@ -0,0 +1,60 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {compose, gql} from 'react-apollo'; +import StorySearch from '../components/StorySearch'; +import withQuery from 'coral-framework/hocs/withQuery'; +import {storySearchChange} from 'coral-admin/src/actions/moderation'; + +class StorySearchContainer extends React.Component { + searchChange = (e) => { + this.props.storySearchChange(e.target.value); + this.props.data.refetch(); + } + + componentDidUpdate (prevProps) { + if (prevProps.moderation.storySearchString !== this.props.moderation.storySearchString) { + this.props.data.refetch(); + } + } + + render () { + return ; + } +} + +export const withAssetSearchQuery = withQuery(gql` + query SearchStories($value: String = "") { + assets(query: {value: $value}) { + id + title + url + created_at + closedAt + author + } + } +`, { + options: ({moderation: {storySearchString = ''}}) => { + return { + variables: { + value: storySearchString + } + }; + } +}); + +const mapStateToProps = (state) => ({ + moderation: state.moderation.toJS() +}); + +const mapDispatchToProps = (dispatch) => ({ + ...bindActionCreators({ + storySearchChange + }, dispatch) +}); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withAssetSearchQuery +)(StorySearchContainer); diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index 11f77df60..b32b37b60 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -19,6 +19,17 @@ const genAssetsByID = (context, ids) => AssetModel.find({ } }).then(util.singleJoinBy(ids, 'id')); +/** + * [getAssetsByQuery description] + * @param {Object} context the context of the request + * @param {String} value text string to search agains the documents + * @param {Number} limit limit the number of results + * @return {Promise} resolves the assets + */ +const getAssetsByQuery = (context, value, limit) => { + return AssetsService.search(value, null, limit); +}; + /** * This endpoint find or creates an asset at the given url when it is loaded. * @param {Object} context the context of the request @@ -65,6 +76,7 @@ module.exports = (context) => ({ // this operation create a new asset if one isn't found. getByURL: (url) => findOrCreateAssetByURL(context, url), + search: (value, limit) => getAssetsByQuery(context, value, limit), 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 e2cd58828..637af9018 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -6,12 +6,14 @@ const { } = require('../../perms/constants'); const RootQuery = { - assets(_, args, {loaders: {Assets}, user}) { + assets(_, {query}, {loaders: {Assets}, user}) { if (user == null || !user.can(SEARCH_ASSETS)) { return null; } - return Assets.getAll.load(); + const {value = '', limit} = query; + + return Assets.search(value, limit); }, asset(_, query, {loaders: {Assets}}) { if (query.id) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 187e5bba5..2da5dca12 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -112,6 +112,16 @@ input UsersQuery { sort: SORT_ORDER = REVERSE_CHRONOLOGICAL } +# AssetsQuery allows teh ability to query assets by specific fields +input AssetsQuery { + + # a search string to match against titles, authors, urls, etc. + value: String = "" + + # Limit the number of results to be returned + limit: Int = 10 +} + ################################################################################ ## Comments ################################################################################ @@ -586,7 +596,7 @@ type RootQuery { comment(id: ID!): Comment # All assets. Requires the `ADMIN` role. - assets: [Asset] + assets(query: AssetsQuery!): [Asset] # Find or create an asset by url, or just find with the ID. asset(id: ID, url: String): Asset From aa289ecbfec038c3ae7786102f1e56aaaf947111 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 9 Jun 2017 14:32:12 -0600 Subject: [PATCH 072/264] less terrible looking --- .../src/routes/Moderation/components/Moderation.js | 2 +- .../Moderation/components/ModerationHeader.js | 4 ++-- .../src/routes/Moderation/components/Story.js | 8 +++++--- .../routes/Moderation/components/StorySearch.css | 13 ++++++++++--- .../src/routes/Moderation/components/StorySearch.js | 9 +++++++-- .../src/routes/Moderation/components/styles.css | 1 - 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index 7582d10d1..511d3f089 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -210,7 +210,7 @@ export default class Moderation extends Component { acceptComment={props.acceptComment} rejectComment={props.rejectComment} /> )} - + ); } diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js b/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js index 9aa9788fb..0ab2eb762 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js @@ -14,8 +14,8 @@ const ModerationHeader = ({asset, searchVisible, openSearch, closeSearch}) => { : ; const title = asset - ? {asset.title} {searchTriggerIcon} - : {t('modqueue.all_streams')} {searchTriggerIcon}; + ? {asset.title} {searchTriggerIcon} + : {t('modqueue.all_streams')} {searchTriggerIcon}; return (
    diff --git a/client/coral-admin/src/routes/Moderation/components/Story.js b/client/coral-admin/src/routes/Moderation/components/Story.js index ee9d462d3..59f6d6fa1 100644 --- a/client/coral-admin/src/routes/Moderation/components/Story.js +++ b/client/coral-admin/src/routes/Moderation/components/Story.js @@ -1,18 +1,20 @@ import React, {PropTypes} from 'react'; +import {Link} from 'react-router'; import styles from './StorySearch.css'; -const Story = ({author, title, createdAt, open}) => { +const Story = ({author, title, createdAt, open, id}) => { return ( -
  • +

    {title}

    By {author}{createdAt}{open ? 'Open' : 'Closed'}

    -
  • + ); }; Story.propTypes = { + id: PropTypes.string.isRequired, author: PropTypes.string.isRequired, title: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired, diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.css b/client/coral-admin/src/routes/Moderation/components/StorySearch.css index e5b6e4a93..ccccd210b 100644 --- a/client/coral-admin/src/routes/Moderation/components/StorySearch.css +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.css @@ -38,32 +38,39 @@ .storyList { padding: 10px; - margin: 0; - list-style: none; border-top: 1px solid #ddd; } .story { padding: 10px; border-bottom: 1px solid #ddd; + cursor: pointer; + display: block; + text-decoration: none; + + &:hover { + background-color: #efefef; + } } .title, .meta { margin: 0; + color: black; } .author, .createdAt, .status { display: inline-block; font-size: .8em; - color: #aaa; } .author { display: inline-block; width: 200px; + color: #aaa; } .createdAt { display: inline-block; width: 200px; + color: #aaa; } diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.js b/client/coral-admin/src/routes/Moderation/components/StorySearch.js index ce16e5b15..a841fd96d 100644 --- a/client/coral-admin/src/routes/Moderation/components/StorySearch.js +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.js @@ -10,6 +10,10 @@ const StorySearch = (props) => { data: {loading} } = props; + if (!props.visible) { + return null; + } + return (
    @@ -19,7 +23,7 @@ const StorySearch = (props) => {

    Moderate comments on All Stories

    -
      +
      { loading ? @@ -27,13 +31,14 @@ const StorySearch = (props) => { const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); return ; }) } -
    +
    diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index a6bf7e89e..b0c2cc5ac 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -471,5 +471,4 @@ span { .searchTrigger { position: relative; top: .3em; - background-color: blue; } From cc4c943a21c7c6950b194c891b7b9ad347dfdbe5 Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Fri, 9 Jun 2017 17:27:35 -0400 Subject: [PATCH 073/264] hover states, link styling, alignment fixes --- .../coral-admin/src/components/ui/Header.css | 2 +- .../routes/Dashboard/components/Widget.css | 22 +++++----- .../Moderation/components/CommentCount.css | 1 + .../Moderation/components/UserDetail.css | 4 +- .../routes/Moderation/components/styles.css | 44 ++++++++++++------- locales/en.yml | 4 +- 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/client/coral-admin/src/components/ui/Header.css b/client/coral-admin/src/components/ui/Header.css index 7cb68ecbf..d99a7f295 100644 --- a/client/coral-admin/src/components/ui/Header.css +++ b/client/coral-admin/src/components/ui/Header.css @@ -77,7 +77,7 @@ letter-spacing: .8; &:hover { - background-color: #232323; + background-color: #404040; } &.active { diff --git a/client/coral-admin/src/routes/Dashboard/components/Widget.css b/client/coral-admin/src/routes/Dashboard/components/Widget.css index 587c51ada..cf83fcbdc 100644 --- a/client/coral-admin/src/routes/Dashboard/components/Widget.css +++ b/client/coral-admin/src/routes/Dashboard/components/Widget.css @@ -17,8 +17,9 @@ .heading { margin: 0; padding-left: 10px; - font-size: 1.5rem; - font-weight: bold; + font-size: 1.3rem; + font-weight: 600; + color: #2c2c2c; } .widgetTable { @@ -32,7 +33,8 @@ } .widgetHead p { - color: rgb(35, 102, 223); + color: #2c2c2c; + font-weight: 500; padding: 10px; text-align: left; text-transform: capitalize; @@ -47,11 +49,11 @@ } .rowLinkify { - cursor: pointer; border-bottom: 1px solid lightgrey; color: #555; height: var(--row-height); padding: 10px; + transition: background-color 200ms; } .rowLinkify:last-child { @@ -60,6 +62,7 @@ .rowLinkify:hover { background-color: #f8f8f8; + pointer: default; } .linkToAsset { @@ -68,16 +71,17 @@ } .linkToModerate { - background-color: #e0e0e0; + background-color: #BDBDBD; padding: 10px 14px; text-decoration: none; color: black; float: right; margin-left: 15px; + transition: background-color 200ms; } .linkToModerate:hover { - background-color: #ccc; + background-color: #9E9E9E; } .lede { @@ -89,14 +93,10 @@ color: #555; text-decoration: none; font-size: 1.2em; - font-weight: normal; + font-weight: 500; margin: 0; } -.assetTitle:hover { - text-decoration: underline; -} - .widgetCount { color: #555; font-size: 1.3em; diff --git a/client/coral-admin/src/routes/Moderation/components/CommentCount.css b/client/coral-admin/src/routes/Moderation/components/CommentCount.css index b684d0f4a..4d24d5702 100644 --- a/client/coral-admin/src/routes/Moderation/components/CommentCount.css +++ b/client/coral-admin/src/routes/Moderation/components/CommentCount.css @@ -12,4 +12,5 @@ right: 0; margin-top: -2px; font-size: 12px; + color: white; } diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.css b/client/coral-admin/src/routes/Moderation/components/UserDetail.css index d4b5f0db7..710f84759 100644 --- a/client/coral-admin/src/routes/Moderation/components/UserDetail.css +++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.css @@ -15,7 +15,7 @@ display: flex; .stat { - margin: 0 4px 12px; + margin: 0 4px 10px 0px; } .stat:last-child { @@ -49,7 +49,7 @@ li { display: inline-block; - margin: 0 10px; + margin-right: 10px; cursor: pointer; padding: 0 10px; } diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index d10311f74..fc825eccb 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -18,14 +18,20 @@ .tab { flex: 1; - color: #EEEEEE; + color: #C0C0C0; text-transform: capitalize; font-weight: 100; - font-size: 15px; + font-size: 14px; letter-spacing: 1px; transition: border-bottom 200ms; - padding: 0px 5px; - margin-right: 30px; + transition: color 200ms; + padding: 0px 10px; + margin-right: 20px; + &:hover { + color: white; + border-bottom: solid 2px #F36451; + box-sizing: border-box; + } } .active { @@ -33,6 +39,10 @@ box-sizing: border-box; border-bottom: solid 4px #F36451; font-weight: 400; + &:hover { + border-bottom: solid 4px #F36451; + font-weight: 400; + } } .active > span { @@ -103,19 +113,18 @@ span { font-weight: 400; font-size: 15px; letter-spacing: 1px; - transition: opacity 200ms; + transition: background-color 200ms; opacity: 1; - &:hover { - opacity: .8; - cursor: pointer; - } - &:first-child { text-align: left; } &:nth-child(2) { + &:hover { + cursor: pointer; + background-color: #212121; + } span { text-align: center; text-overflow: ellipsis; @@ -245,7 +254,6 @@ span { padding: 5px; color: #262626; font-size: 14px; - margin-left: 15px; line-height: 1px; font-weight: 300; } @@ -421,17 +429,21 @@ span { .tabIcon { position: relative; - top: 2px; + top: 3px; font-size: 16px; } .username { - color: blue; - text-decoration: underline; + color: #393B44; + text-decoration: none; cursor: pointer; - + font-weight: 600; + padding: 2px 5px; + border-radius: 2px; + margin-left: -5px; + transition: background-color 200ms ease; &:hover { - background-color: rgba(255, 0, 0, .1); + background-color: #E0E0E0; } } diff --git a/locales/en.yml b/locales/en.yml index ce9abdce5..930a4eadb 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -144,8 +144,8 @@ en: auto_update: "Data automatically updates every five minutes or when you Reload." comment_count: comments flags: Flags - most_flags: "Articles with the most flags" - most_conversations: "Articles with the most conversations" + most_flags: "Stories with the most flags" + most_conversations: "Stories with the most conversations" next_update: "{0} minutes until next update." no_activity: "There haven't been any comments anywhere in the last five minutes." no_flags: "There have been no flags in the last 5 minutes! Hooray!" From 74ad3c3a35bc11ca806ba9db3285a0edc3230937 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 18:35:53 +0700 Subject: [PATCH 074/264] Refactor all reactions and encapsulate server code --- plugin-api/beta/server/getReactionConfig.js | 113 +++++++++++ plugin-api/beta/server/index.js | 3 + .../coral-plugin-like/client/LikeButton.js | 55 ++++++ .../client/components/LikeButton.js | 86 -------- .../client/containers/LikeButton.js | 186 ------------------ plugins/coral-plugin-like/client/index.js | 4 +- .../{components/style.css => styles.css} | 9 +- .../client/translations.json | 10 - .../coral-plugin-like/client/translations.yml | 9 + plugins/coral-plugin-like/index.js | 38 +--- .../coral-plugin-like/server/typeDefs.graphql | 70 ------- .../coral-plugin-love/client/LoveButton.js | 23 ++- plugins/coral-plugin-love/client/index.js | 2 +- plugins/coral-plugin-love/client/styles.css | 37 ++-- .../client/translations.json | 10 - .../coral-plugin-love/client/translations.yml | 9 + plugins/coral-plugin-love/index.js | 38 +--- .../coral-plugin-love/server/typeDefs.graphql | 70 ------- .../client}/Icon.js | 0 .../client/RespectButton.js | 57 ++++++ .../client/components/Icon.js | 6 - .../client/components/RespectButton.js | 69 ------- .../client/containers/RespectButton.js | 163 --------------- plugins/coral-plugin-respect/client/index.js | 6 +- .../{components/style.css => styles.css} | 4 +- .../client/translations.json | 10 - .../client/translations.yml | 9 + plugins/coral-plugin-respect/index.js | 43 +--- .../server/typeDefs.graphql | 54 ----- 29 files changed, 303 insertions(+), 890 deletions(-) create mode 100644 plugin-api/beta/server/getReactionConfig.js create mode 100644 plugin-api/beta/server/index.js create mode 100644 plugins/coral-plugin-like/client/LikeButton.js delete mode 100644 plugins/coral-plugin-like/client/components/LikeButton.js delete mode 100644 plugins/coral-plugin-like/client/containers/LikeButton.js rename plugins/coral-plugin-like/client/{components/style.css => styles.css} (89%) delete mode 100644 plugins/coral-plugin-like/client/translations.json create mode 100644 plugins/coral-plugin-like/client/translations.yml delete mode 100644 plugins/coral-plugin-like/server/typeDefs.graphql delete mode 100644 plugins/coral-plugin-love/client/translations.json create mode 100644 plugins/coral-plugin-love/client/translations.yml delete mode 100644 plugins/coral-plugin-love/server/typeDefs.graphql rename plugins/{coral-plugin-like/client/components => coral-plugin-respect/client}/Icon.js (100%) create mode 100644 plugins/coral-plugin-respect/client/RespectButton.js delete mode 100644 plugins/coral-plugin-respect/client/components/Icon.js delete mode 100644 plugins/coral-plugin-respect/client/components/RespectButton.js delete mode 100644 plugins/coral-plugin-respect/client/containers/RespectButton.js rename plugins/coral-plugin-respect/client/{components/style.css => styles.css} (95%) delete mode 100644 plugins/coral-plugin-respect/client/translations.json create mode 100644 plugins/coral-plugin-respect/client/translations.yml delete mode 100644 plugins/coral-plugin-respect/server/typeDefs.graphql diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js new file mode 100644 index 000000000..101c86782 --- /dev/null +++ b/plugin-api/beta/server/getReactionConfig.js @@ -0,0 +1,113 @@ +const wrapResponse = require('../../../graph/helpers/response'); + +function getReactionConfig(reaction) { + const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1); + const REACTION = reaction.toUpperCase(); + const typeDefs = ` + enum ACTION_TYPE { + + # Represents a ${Reaction}. + ${REACTION} + } + + enum ASSET_METRICS_SORT { + + # Represents a ${Reaction}Action. + ${REACTION} + } + + input Create${Reaction}Input { + + # The item's id for which we are to create a ${reaction}. + item_id: ID! + + # The type of the item for which we are to create the ${reaction}. + item_type: ACTION_ITEM_TYPE! + } + + # ${Reaction}Action is used by users who "${reaction}" a specific entity. + type ${Reaction}Action implements Action { + + # The ID of the action. + id: ID! + + # The author of the action. + user: User + + # The time when the Action was updated. + updated_at: Date + + # The time when the Action was created. + created_at: Date + } + + type ${Reaction}ActionSummary implements ActionSummary { + + # The count of actions with this group. + count: Int + + # The current user's action. + current_user: ${Reaction}Action + } + + # A summary of counts related to all the ${Reaction}s on an Asset. + type ${Reaction}AssetActionSummary implements AssetActionSummary { + + # Number of ${reaction}s associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the ${reaction}s. + actionableItemCount: Int + } + + type Create${Reaction}Response implements Response { + + # The ${reaction} that was created. + ${reaction}: ${Reaction}Action + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] + } + + type RootMutation { + + # Creates a ${reaction} on an entity. + create${Reaction}(${reaction}: Create${Reaction}Input!): Create${Reaction}Response + } + `; + + return { + typeDefs, + resolvers: { + RootMutation: { + [`create${Reaction}`]: (_, {[reaction]: {item_id, item_type}}, {mutators: {Action}}) => { + return wrapResponse(reaction)(Action.create({item_id, item_type, action_type: REACTION})); + } + }, + }, + hooks: { + Action: { + __resolveType: { + post({action_type}) { + switch (action_type) { + case REACTION: + return `${Reaction}Action`; + } + } + } + }, + ActionSummary: { + __resolveType: { + post({action_type}) { + switch (action_type) { + case REACTION: + return `${Reaction}ActionSummary`; + } + } + } + } + } + }; +} + +module.exports = getReactionConfig; diff --git a/plugin-api/beta/server/index.js b/plugin-api/beta/server/index.js new file mode 100644 index 000000000..b32d78a45 --- /dev/null +++ b/plugin-api/beta/server/index.js @@ -0,0 +1,3 @@ +module.exports = { + getReactionConfig: require('./getReactionConfig'), +}; diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js new file mode 100644 index 000000000..fac4fdfca --- /dev/null +++ b/plugins/coral-plugin-like/client/LikeButton.js @@ -0,0 +1,55 @@ +import React from 'react'; +import styles from './styles.css'; +import {withReaction} from 'plugin-api/beta/client/hocs'; +import {t, can} from 'plugin-api/beta/client/services'; +import {Icon} from 'plugin-api/beta/client/components'; +import cn from 'classnames'; + +const plugin = 'coral-plugin-like'; + +class LikeButton extends React.Component { + handleClick = () => { + const { + postReaction, + deleteReaction, + showSignInDialog, + alreadyReacted, + user, + } = this.props; + + // If the current user does not exist, trigger sign in dialog. + if (!user) { + showSignInDialog(); + return; + } + + // If the current user is suspended, do nothing. + if (!can(user, 'INTERACT_WITH_COMMUNITY')) { + return; + } + + if (alreadyReacted()) { + deleteReaction(); + } else { + postReaction(); + } + }; + + render() { + const {count, alreadyReacted} = this.props; + return ( +
    + +
    + ); + } +} + +export default withReaction('like')(LikeButton); diff --git a/plugins/coral-plugin-like/client/components/LikeButton.js b/plugins/coral-plugin-like/client/components/LikeButton.js deleted file mode 100644 index 7b66a11d4..000000000 --- a/plugins/coral-plugin-like/client/components/LikeButton.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, {Component} from 'react'; -import styles from './style.css'; - -import cn from 'classnames'; -import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; -import t from 'coral-framework/services/i18n'; - -const name = 'coral-plugin-like'; - -class LikeButton extends Component { - handleClick = () => { - const {postLike, showSignInDialog, deleteAction} = this.props; - const {root: {me}, comment} = this.props; - - const myLikeActionSummary = getMyActionSummary( - 'LikeActionSummary', - comment - ); - - // If the current user does not exist, trigger sign in dialog. - if (!me) { - showSignInDialog(); - return; - } - - // If the current user is banned, do nothing. - if (me.status === 'BANNED') { - return; - } - - if (myLikeActionSummary) { - deleteAction(myLikeActionSummary.current_user.id, comment.id); - } else { - postLike({ - item_id: comment.id, - item_type: 'COMMENTS' - }); - } - }; - - render() { - const {comment} = this.props; - - if (!comment) { - return null; - } - - const myLike = getMyActionSummary('LikeActionSummary', comment); - let count = getTotalActionCount('LikeActionSummary', comment); - - return ( -
    - -
    - ); - } -} - -LikeButton.propTypes = { - data: React.PropTypes.object.isRequired -}; - -export default LikeButton; diff --git a/plugins/coral-plugin-like/client/containers/LikeButton.js b/plugins/coral-plugin-like/client/containers/LikeButton.js deleted file mode 100644 index 51bee1b40..000000000 --- a/plugins/coral-plugin-like/client/containers/LikeButton.js +++ /dev/null @@ -1,186 +0,0 @@ -import get from 'lodash/get'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import {compose, gql, graphql} from 'react-apollo'; -import LikeButton from '../components/LikeButton'; -import withFragments from 'coral-framework/hocs/withFragments'; -import{showSignInDialog} from 'coral-framework/actions/auth'; - -const isLikeAction = (a) => a.__typename === 'LikeActionSummary'; - -const COMMENT_FRAGMENT = gql` - fragment LikeButton_updateFragment on Comment { - action_summaries { - ... on LikeActionSummary { - count - current_user { - id - } - } - } - } -`; - -const withDeleteAction = graphql( - gql` - mutation deleteAction($id: ID!) { - deleteAction(id:$id) { - errors { - translation_key - } - } - } -`, - { - props: ({mutate}) => ({ - deleteAction: (id, commentId) => { - return mutate({ - variables: {id}, - optimisticResponse: { - deleteAction: { - __typename: 'DeleteActionResponse', - errors: null - } - }, - update: (proxy) => { - const fragmentId = `Comment_${commentId}`; - - // Read the data from our cache for this query. - const data = proxy.readFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId - }); - - // Check whether we liked this comment. - const idx = data.action_summaries.findIndex(isLikeAction); - if ( - idx < 0 || - get(data.action_summaries[idx], 'current_user.id') !== id - ) { - return; - } - - data.action_summaries[idx] = { - ...data.action_summaries[idx], - count: data.action_summaries[idx].count - 1, - current_user: null - }; - - // Write our data back to the cache. - proxy.writeFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId, - data - }); - } - }); - } - }) - } -); - -const withPostLike = graphql( - gql` - mutation createLike($like: CreateLikeInput!) { - createLike(like: $like) { - like { - id - } - errors { - translation_key - } - } - } -`, - { - props: ({mutate}) => ({ - postLike: (like) => { - return mutate({ - variables: {like}, - optimisticResponse: { - createLike: { - __typename: 'CreateLikeResponse', - errors: null, - like: { - __typename: 'LikeAction', - id: 'pending' - } - } - }, - update: (proxy, mutationResult) => { - const fragmentId = `Comment_${like.item_id}`; - - // Read the data from our cache for this query. - const data = proxy.readFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId - }); - - // Add our comment from the mutation to the end. - let idx = data.action_summaries.findIndex(isLikeAction); - - // Check whether we already liked this comment. - if (idx >= 0 && data.action_summaries[idx].current_user) { - return; - } - - if (idx < 0) { - - // Add initial action when it doesn't exist. - data.action_summaries.push({ - __typename: 'LikeActionSummary', - count: 0, - current_user: null - }); - idx = data.action_summaries.length - 1; - } - - data.action_summaries[idx] = { - ...data.action_summaries[idx], - count: data.action_summaries[idx].count + 1, - current_user: mutationResult.data.createLike.like - }; - - // Write our data back to the cache. - proxy.writeFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId, - data - }); - } - }); - } - }) - } -); - -const mapDispatchToProps = (dispatch) => - bindActionCreators({showSignInDialog}, dispatch); - -const enhance = compose( - withFragments({ - root: gql` - fragment LikeButton_root on RootQuery { - me { - status - } - } - `, - comment: gql` - fragment LikeButton_comment on Comment { - action_summaries { - ... on LikeActionSummary { - count - current_user { - id - } - } - } - }` - }), - connect(null, mapDispatchToProps), - withDeleteAction, - withPostLike -); - -export default enhance(LikeButton); diff --git a/plugins/coral-plugin-like/client/index.js b/plugins/coral-plugin-like/client/index.js index 86d20863a..68b7a2c46 100644 --- a/plugins/coral-plugin-like/client/index.js +++ b/plugins/coral-plugin-like/client/index.js @@ -1,5 +1,5 @@ -import LikeButton from './containers/LikeButton'; -import translations from './translations.json'; +import LikeButton from './LikeButton'; +import translations from './translations.yml'; export default { translations, diff --git a/plugins/coral-plugin-like/client/components/style.css b/plugins/coral-plugin-like/client/styles.css similarity index 89% rename from plugins/coral-plugin-like/client/components/style.css rename to plugins/coral-plugin-like/client/styles.css index f45df86ef..cb372fa47 100644 --- a/plugins/coral-plugin-like/client/components/style.css +++ b/plugins/coral-plugin-like/client/styles.css @@ -1,6 +1,6 @@ -.like { +.container { display: inline-block; - } +} .button { color: #2a2a2a; @@ -17,14 +17,9 @@ &.liked { color: rgb(0,134,227); - &:hover { color: rgb(0,134,227); cursor: pointer; } } } - -.icon { - padding: 0 5px; -} diff --git a/plugins/coral-plugin-like/client/translations.json b/plugins/coral-plugin-like/client/translations.json deleted file mode 100644 index 93d73d3a2..000000000 --- a/plugins/coral-plugin-like/client/translations.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "en": { - "like": "Like", - "liked": "Liked" - }, - "es": { - "like": "Me Gusta", - "liked": "Me Gustó" - } -} diff --git a/plugins/coral-plugin-like/client/translations.yml b/plugins/coral-plugin-like/client/translations.yml new file mode 100644 index 000000000..c534663af --- /dev/null +++ b/plugins/coral-plugin-like/client/translations.yml @@ -0,0 +1,9 @@ +en: + coral-plugin-like: + like: Like + liked: Liked +es: + coral-plugin-like: + like: Me Gusta + liked: Me Gustó + diff --git a/plugins/coral-plugin-like/index.js b/plugins/coral-plugin-like/index.js index 4fc0285f4..690577bed 100644 --- a/plugins/coral-plugin-like/index.js +++ b/plugins/coral-plugin-like/index.js @@ -1,36 +1,2 @@ -const {readFileSync} = require('fs'); -const path = require('path'); -const wrapResponse = require('../../graph/helpers/response'); - -module.exports = { - typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'), - resolvers: { - RootMutation: { - createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) { - return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'})); - } - } - }, - hooks: { - Action: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LIKE': - return 'LikeAction'; - } - } - } - }, - ActionSummary: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LIKE': - return 'LikeActionSummary'; - } - } - } - } - } -}; +const {getReactionConfig} = require('../../plugin-api/beta/server'); +module.exports = getReactionConfig('like'); diff --git a/plugins/coral-plugin-like/server/typeDefs.graphql b/plugins/coral-plugin-like/server/typeDefs.graphql deleted file mode 100644 index 40c600f2f..000000000 --- a/plugins/coral-plugin-like/server/typeDefs.graphql +++ /dev/null @@ -1,70 +0,0 @@ -enum ACTION_TYPE { - - # Represents a Like. - LIKE -} - -enum ASSET_METRICS_SORT { - - # Represents a LikeAction. - LIKE -} - -input CreateLikeInput { - - # The item's id for which we are to create a like. - item_id: ID! - - # The type of the item for which we are to create the like. - item_type: ACTION_ITEM_TYPE! -} - -# LikeAction is used by users who "like" a specific entity. -type LikeAction implements Action { - - # The ID of the action. - id: ID! - - # The author of the action. - user: User - - # The time when the Action was updated. - updated_at: Date - - # The time when the Action was created. - created_at: Date -} - -type LikeActionSummary implements ActionSummary { - - # The count of actions with this group. - count: Int - - # The current user's action. - current_user: LikeAction -} - -# 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 -} - -type CreateLikeResponse implements Response { - - # The like that was created. - like: LikeAction - - # An array of errors relating to the mutation that occurred. - errors: [UserError!] -} - -type RootMutation { - - # Creates a like on an entity. - createLike(like: CreateLikeInput!): CreateLikeResponse -} diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js index de4c4c9e7..8dbf51a6e 100644 --- a/plugins/coral-plugin-love/client/LoveButton.js +++ b/plugins/coral-plugin-love/client/LoveButton.js @@ -1,8 +1,11 @@ import React from 'react'; -import {Icon} from 'coral-ui'; import styles from './styles.css'; import {withReaction} from 'plugin-api/beta/client/hocs'; import {t, can} from 'plugin-api/beta/client/services'; +import {Icon} from 'plugin-api/beta/client/components'; +import cn from 'classnames'; + +const plugin = 'coral-plugin-love'; class LoveButton extends React.Component { handleClick = () => { @@ -35,14 +38,16 @@ class LoveButton extends React.Component { render() { const {count, alreadyReacted} = this.props; return ( - +
    + +
    ); } } diff --git a/plugins/coral-plugin-love/client/index.js b/plugins/coral-plugin-love/client/index.js index fa2f71159..fd7174d81 100644 --- a/plugins/coral-plugin-love/client/index.js +++ b/plugins/coral-plugin-love/client/index.js @@ -1,5 +1,5 @@ import LoveButton from './LoveButton'; -import translations from './translations.json'; +import translations from './translations.yml'; export default { translations, diff --git a/plugins/coral-plugin-love/client/styles.css b/plugins/coral-plugin-love/client/styles.css index d48e7e28c..e16e17ca4 100644 --- a/plugins/coral-plugin-love/client/styles.css +++ b/plugins/coral-plugin-love/client/styles.css @@ -1,26 +1,25 @@ -.respect { - display: inline-block; +.container { + display: inline-block; } .button { - color: #2a2a2a; - margin: 5px 10px 5px 0px; - background: none; - padding: 0px; - border: none; - font-size: inherit; + color: #2a2a2a; + margin: 5px 10px 5px 0px; + background: none; + padding: 0px; + border: none; + font-size: inherit; + &:hover { + color: #767676; + cursor: pointer; + } + + &.loved { + color: #e52338; &:hover { - color: #767676; - cursor: pointer; - } - - &.loved { - color: #e52338; - - &:hover { - color: #e52839; - cursor: pointer; - } + color: #e52839; + cursor: pointer; } + } } diff --git a/plugins/coral-plugin-love/client/translations.json b/plugins/coral-plugin-love/client/translations.json deleted file mode 100644 index a015efa77..000000000 --- a/plugins/coral-plugin-love/client/translations.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "en": { - "love": "Love", - "loved": "Loved" - }, - "es": { - "love": "Amo", - "loved": "Amé" - } -} diff --git a/plugins/coral-plugin-love/client/translations.yml b/plugins/coral-plugin-love/client/translations.yml new file mode 100644 index 000000000..06b6e5257 --- /dev/null +++ b/plugins/coral-plugin-love/client/translations.yml @@ -0,0 +1,9 @@ +en: + coral-plugin-love: + love: Love + loved: Loved +es: + coral-plugin-love: + love: Amo + loved: Amé + diff --git a/plugins/coral-plugin-love/index.js b/plugins/coral-plugin-love/index.js index 543c2a099..b57fdec39 100644 --- a/plugins/coral-plugin-love/index.js +++ b/plugins/coral-plugin-love/index.js @@ -1,36 +1,2 @@ -const {readFileSync} = require('fs'); -const path = require('path'); -const wrapResponse = require('../../graph/helpers/response'); - -module.exports = { - typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'), - resolvers: { - RootMutation: { - createLove(_, {love: {item_id, item_type}}, {mutators: {Action}}) { - return wrapResponse('love')(Action.create({item_id, item_type, action_type: 'LOVE'})); - } - } - }, - hooks: { - Action: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LOVE': - return 'LoveAction'; - } - } - } - }, - ActionSummary: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LOVE': - return 'LoveActionSummary'; - } - } - } - } - } -}; +const {getReactionConfig} = require('../../plugin-api/beta/server'); +module.exports = getReactionConfig('love'); diff --git a/plugins/coral-plugin-love/server/typeDefs.graphql b/plugins/coral-plugin-love/server/typeDefs.graphql deleted file mode 100644 index edc45e20b..000000000 --- a/plugins/coral-plugin-love/server/typeDefs.graphql +++ /dev/null @@ -1,70 +0,0 @@ -enum ACTION_TYPE { - - # Represents a Love. - LOVE -} - -enum ASSET_METRICS_SORT { - - # Represents a LoveAction. - LOVE -} - -input CreateLoveInput { - - # The item's id for which we are to create a love. - item_id: ID! - - # The type of the item for which we are to create the love. - item_type: ACTION_ITEM_TYPE! -} - -# LoveAction is used by users who "love" a specific entity. -type LoveAction implements Action { - - # The ID of the action. - id: ID! - - # The author of the action. - user: User - - # The time when the Action was updated. - updated_at: Date - - # The time when the Action was created. - created_at: Date -} - -type LoveActionSummary implements ActionSummary { - - # The count of actions with this group. - count: Int - - # The current user's action. - current_user: LoveAction -} - -# A summary of counts related to all the Loves on an Asset. -type LoveAssetActionSummary implements AssetActionSummary { - - # Number of loves associated with actionable types on this this Asset. - actionCount: Int - - # Number of unique actionable types that are referenced by the loves. - actionableItemCount: Int -} - -type CreateLoveResponse implements Response { - - # The love that was created. - love: LoveAction - - # An array of errors relating to the mutation that occurred. - errors: [UserError!] -} - -type RootMutation { - - # Creates a love on an entity. - createLove(love: CreateLoveInput!): CreateLoveResponse -} diff --git a/plugins/coral-plugin-like/client/components/Icon.js b/plugins/coral-plugin-respect/client/Icon.js similarity index 100% rename from plugins/coral-plugin-like/client/components/Icon.js rename to plugins/coral-plugin-respect/client/Icon.js diff --git a/plugins/coral-plugin-respect/client/RespectButton.js b/plugins/coral-plugin-respect/client/RespectButton.js new file mode 100644 index 000000000..14b0ec057 --- /dev/null +++ b/plugins/coral-plugin-respect/client/RespectButton.js @@ -0,0 +1,57 @@ +import React from 'react'; +import Icon from './Icon'; +import styles from './styles.css'; +import {withReaction} from 'plugin-api/beta/client/hocs'; +import {t, can} from 'plugin-api/beta/client/services'; +import cn from 'classnames'; + +const plugin = 'coral-plugin-respect'; + +class RespectButton extends React.Component { + handleClick = () => { + const { + postReaction, + deleteReaction, + showSignInDialog, + alreadyReacted, + user, + } = this.props; + + // If the current user does not exist, trigger sign in dialog. + if (!user) { + showSignInDialog(); + return; + } + + // If the current user is suspended, do nothing. + if (!can(user, 'INTERACT_WITH_COMMUNITY')) { + return; + } + + if (alreadyReacted()) { + deleteReaction(); + } else { + postReaction(); + } + }; + + render() { + const {count, alreadyReacted} = this.props; + return ( +
    + +
    + ); + } +} + +export default withReaction('respect')(RespectButton); diff --git a/plugins/coral-plugin-respect/client/components/Icon.js b/plugins/coral-plugin-respect/client/components/Icon.js deleted file mode 100644 index c24841e97..000000000 --- a/plugins/coral-plugin-respect/client/components/Icon.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; - -export default ({className}) => ( -