From 1137fca3b4d5b6f059ae10cb757eed5f61da0e25 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 18 Apr 2017 14:03:26 -0700 Subject: [PATCH 01/91] 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 02/91] 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 03/91] 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 04/91] 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 05/91] 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 06/91] 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 07/91] 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 08/91] 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 09/91] 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 10/91] 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 11/91] 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 12/91] 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 13/91] 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 14/91] 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 15/91] 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 16/91] 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 17/91] 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 18/91] 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 19/91] 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 20/91] 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 21/91] 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 22/91] 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 23/91] 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 24/91] 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 25/91] 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 26/91] 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 27/91] 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 28/91] 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 29/91] 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 30/91] 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 31/91] 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 32/91] 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 33/91] 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 34/91] 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 5faa92e8b394edc13c795e8573150f7776d1e959 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 6 Jun 2017 08:09:34 -0300 Subject: [PATCH 35/91] Label style, top level comments, no offtopic replies --- client/coral-embed-stream/src/components/Comment.js | 6 ++++-- client/coral-framework/components/Slot.css | 4 ++++ client/coral-framework/components/Slot.js | 4 ++-- .../client/components/OffTopicCheckbox.js | 12 ++++++++---- .../client/components/OffTopicFilter.js | 7 +++++++ .../client/components/OffTopicTag.js | 2 +- .../client/components/styles.css | 5 ++++- 7 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 48392cc51..d8410becd 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -252,11 +252,13 @@ class Comment extends React.Component { { (currentUser && diff --git a/client/coral-framework/components/Slot.css b/client/coral-framework/components/Slot.css index 6a95d79ad..91569b717 100644 --- a/client/coral-framework/components/Slot.css +++ b/client/coral-framework/components/Slot.css @@ -2,6 +2,10 @@ display: inline-block; } +.right { + float: right; +} + .debug { background-color: coral; } \ No newline at end of file diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 939068e83..342532bdb 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -4,9 +4,9 @@ import styles from './Slot.css'; import {connect} from 'react-redux'; import {getSlotElements} from 'coral-framework/helpers/plugins'; -function Slot ({fill, inline = false, plugin_config: config, ...rest}) { +function Slot ({fill, inline = false, right = false, plugin_config: config, ...rest}) { return ( -
    +
    {getSlotElements(fill, {...rest, config})}
    ); diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js index 0383507d1..c05b1d8b1 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicCheckbox.js @@ -22,10 +22,14 @@ class OffTopicCheckbox extends React.Component { render() { return (
    - + { + !this.props.isReply ? ( + + ) : null + }
    ); } diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js b/plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js new file mode 100644 index 000000000..24718d1cb --- /dev/null +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js @@ -0,0 +1,7 @@ +import React from 'react'; + +export default (props) => ( + + Filter + +); diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js index 89a4d03aa..62d8e1a0f 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicTag.js @@ -10,7 +10,7 @@ const isOffTopic = (tags) => { export default (props) => ( { - isOffTopic(props.comment.tags) ? ( + isOffTopic(props.comment.tags) && props.depth === 0 ? ( {t('off_topic')} diff --git a/plugins/coral-plugin-offtopic/client/components/styles.css b/plugins/coral-plugin-offtopic/client/components/styles.css index f3e290b09..52c36e6c2 100644 --- a/plugins/coral-plugin-offtopic/client/components/styles.css +++ b/plugins/coral-plugin-offtopic/client/components/styles.css @@ -8,7 +8,10 @@ } .tag { - background: rgba(245, 188, 168, 0.6); + background: #3D73D5; + font-size: 12px; + font-weight: bold; + color: white; display: inline-block; margin: 0px 5px; padding: 5px 5px; From 3ad3b3af1cac1ebb6faa27a4c97e1cbcc1dbd662 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 6 Jun 2017 09:43:34 -0300 Subject: [PATCH 36/91] Adding filters --- .../src/components/Stream.js | 19 +++++--- .../src/components/ViewingOptions.css | 23 ++++++++++ .../src/components/ViewingOptions.js | 44 +++++++++++++++++++ client/coral-embed-stream/style/default.css | 9 ++++ client/coral-plugin-moderation/styles.css | 2 + .../client/components/OffTopicFilter.js | 6 +-- plugins/coral-plugin-offtopic/client/index.js | 2 + 7 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 client/coral-embed-stream/src/components/ViewingOptions.css create mode 100644 client/coral-embed-stream/src/components/ViewingOptions.js diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 11f2b7819..131b56255 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -6,6 +6,7 @@ import Comment from '../containers/Comment'; import SuspendedAccount from './SuspendedAccount'; import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox'; +import ViewingOptions from './ViewingOptions'; import Slot from 'coral-framework/components/Slot'; import InfoBox from 'coral-plugin-infobox/InfoBox'; import {can} from 'coral-framework/services/perms'; @@ -13,6 +14,7 @@ import {ModerationLink} from 'coral-plugin-moderation'; import CommentBox from 'coral-plugin-commentbox/CommentBox'; import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; + import t, {timeago} from 'coral-framework/services/i18n'; class Stream extends React.Component { @@ -108,11 +110,16 @@ class Stream extends React.Component { />}
    :

    {asset.settings.closedMessage}

    } - {loggedIn && - } + +
    + {loggedIn && + } + + +
    {/* the highlightedComment is isolated after the user followed a permalink */} {highlightedComment @@ -140,7 +147,7 @@ class Stream extends React.Component { maxCharCount={asset.settings.charCount} editComment={this.props.editComment} /> - :
    + :
    { + this.setState((state) => ({ + open: !state.open + })); + } + + render() { + return ( +
    + + { + this.state.open ? ( +
    +
      + { + React.Children.map(, (component) => { + return React.createElement('li', { + className: 'viewingOption' + }, component); + }) + } +
    +
    + ) : null + } +
    + ); + } +} diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 04b2536a3..b24619c57 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -178,6 +178,15 @@ hr { display: none; } +.streamBox { + padding: 10px 0; +} + +.commentStreamContainer { + position: relative; + z-index: -1; +} + .commentStream { /* prevent absolutely positioned final permalink popover from being clipped */ padding-bottom: 50px; diff --git a/client/coral-plugin-moderation/styles.css b/client/coral-plugin-moderation/styles.css index 2e928d3df..734cc1e6f 100644 --- a/client/coral-plugin-moderation/styles.css +++ b/client/coral-plugin-moderation/styles.css @@ -1,4 +1,6 @@ .moderationLink { + display: inline; + a { color: #679af3; text-decoration: none; diff --git a/plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js b/plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js index 24718d1cb..fb9e6eb7a 100644 --- a/plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js +++ b/plugins/coral-plugin-offtopic/client/components/OffTopicFilter.js @@ -1,7 +1,5 @@ import React from 'react'; -export default (props) => ( - - Filter - +export default () => ( + Hide Off-Topic Comments ); diff --git a/plugins/coral-plugin-offtopic/client/index.js b/plugins/coral-plugin-offtopic/client/index.js index f3459441d..cd591de28 100644 --- a/plugins/coral-plugin-offtopic/client/index.js +++ b/plugins/coral-plugin-offtopic/client/index.js @@ -1,11 +1,13 @@ import OffTopicCheckbox from './components/OffTopicCheckbox'; import OffTopicTag from './components/OffTopicTag'; +import OffTopicFilter from './components/OffTopicFilter'; import translations from './translations.json'; export default { translations, slots: { commentInputDetailArea: [OffTopicCheckbox], + streamViewingOptions: [OffTopicFilter], commentInfoBar: [OffTopicTag] } }; From 9e001f5ce9c107277eaa86abecff5e80e5231452 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 6 Jun 2017 11:15:35 -0300 Subject: [PATCH 37/91] addClassNames, removeClassNames, reducers and actions for Comment Component --- .../coral-embed-stream/src/actions/comment.js | 11 +++++ .../src/components/Comment.js | 46 ++++++++++--------- .../src/components/ViewingOptions.css | 2 + .../src/components/ViewingOptions.js | 2 +- .../src/constants/comment.js | 2 + .../src/reducers/comment.js | 25 ++++++++++ .../coral-embed-stream/src/reducers/index.js | 4 +- .../client/components/OffTopicFilter.js | 38 +++++++++++++-- .../client/components/styles.css | 4 ++ 9 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 client/coral-embed-stream/src/actions/comment.js create mode 100644 client/coral-embed-stream/src/constants/comment.js create mode 100644 client/coral-embed-stream/src/reducers/comment.js diff --git a/client/coral-embed-stream/src/actions/comment.js b/client/coral-embed-stream/src/actions/comment.js new file mode 100644 index 000000000..574d66267 --- /dev/null +++ b/client/coral-embed-stream/src/actions/comment.js @@ -0,0 +1,11 @@ +import {ADD_CLASSNAME, REMOVE_CLASSNAME} from '../constants/comment'; + +export const addClassName = (className) => ({ + type: ADD_CLASSNAME, + className +}); + +export const removeClassName = (idx) => ({ + type: REMOVE_CLASSNAME, + idx +}); diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index d8410becd..3b8b8b08d 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -1,4 +1,6 @@ import React, {PropTypes} from 'react'; +import {connect} from 'react-redux'; +import cn from 'classnames'; import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton'; import AuthorName from 'coral-plugin-author-name/AuthorName'; @@ -19,7 +21,6 @@ import Slot from 'coral-framework/components/Slot'; import LoadMore from './LoadMore'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import {TopRightMenu} from './TopRightMenu'; -import classnames from 'classnames'; import {EditableCommentContent} from './EditableCommentContent'; import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import {getEditableUntilDate} from './util'; @@ -151,28 +152,29 @@ class Comment extends React.Component { } render () { const { - comment, - parentId, - currentUser, asset, depth, - postComment, - addNotification, - showSignInDialog, - highlighted, + comment, postFlag, - postDontAgree, + parentId, loadMore, - setActiveReplyBox, - activeReplyBox, - deleteAction, - addCommentTag, - removeCommentTag, ignoreUser, + highlighted, + postComment, + currentUser, + deleteAction, disableReply, - commentIsIgnored, maxCharCount, - charCountEnable + postDontAgree, + addCommentTag, + activeReplyBox, + addNotification, + charCountEnable, + classNames = [], + showSignInDialog, + removeCommentTag, + commentIsIgnored, + setActiveReplyBox, } = this.props; const flagSummary = getActionSummary('FlagActionSummary', comment); @@ -226,7 +228,7 @@ class Comment extends React.Component { return (
    @@ -265,17 +267,17 @@ class Comment extends React.Component { (comment.user.id === currentUser.id)) /* User can edit/delete their own comment for a short window after posting */ - ? + ? { commentIsStillEditable(comment) && Edit } /* TopRightMenu allows currentUser to ignore other users' comments */ - : + : ({classNames: comment.classNames}); + +export default connect(mapStateToProps, null)(Comment); // return whether the comment is editable function commentIsStillEditable (comment) { diff --git a/client/coral-embed-stream/src/components/ViewingOptions.css b/client/coral-embed-stream/src/components/ViewingOptions.css index 45c45c00c..ba9de723b 100644 --- a/client/coral-embed-stream/src/components/ViewingOptions.css +++ b/client/coral-embed-stream/src/components/ViewingOptions.css @@ -10,6 +10,8 @@ background: white; position: absolute; box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); + right: 3px; + top: 20px; } .streamViewingOptionsList ul, .streamViewingOptionsList li { diff --git a/client/coral-embed-stream/src/components/ViewingOptions.js b/client/coral-embed-stream/src/components/ViewingOptions.js index 70b637759..d0fdeaf5b 100644 --- a/client/coral-embed-stream/src/components/ViewingOptions.js +++ b/client/coral-embed-stream/src/components/ViewingOptions.js @@ -24,7 +24,7 @@ export default class ViewingOptions extends React.Component { Viewing Options
    { - this.state.open ? ( + true ? (