diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/accountSettings.js | 8 | ||||
-rw-r--r-- | models/activities.js | 6 | ||||
-rw-r--r-- | models/announcements.js | 36 | ||||
-rw-r--r-- | models/attachments.js | 153 | ||||
-rw-r--r-- | models/boards.js | 245 | ||||
-rw-r--r-- | models/cardComments.js | 138 | ||||
-rw-r--r-- | models/cards.js | 132 | ||||
-rw-r--r-- | models/checklistItems.js | 145 | ||||
-rw-r--r-- | models/checklists.js | 252 | ||||
-rw-r--r-- | models/export.js | 4 | ||||
-rw-r--r-- | models/integrations.js | 264 | ||||
-rw-r--r-- | models/invitationCodes.js | 4 | ||||
-rw-r--r-- | models/lists.js | 198 | ||||
-rw-r--r-- | models/settings.js | 28 | ||||
-rw-r--r-- | models/swimlanes.js | 219 | ||||
-rw-r--r-- | models/trelloCreator.js | 72 | ||||
-rw-r--r-- | models/users.js | 319 | ||||
-rw-r--r-- | models/wekanCreator.js | 150 |
18 files changed, 1659 insertions, 714 deletions
diff --git a/models/accountSettings.js b/models/accountSettings.js index db4087c0..6dfbac5d 100644 --- a/models/accountSettings.js +++ b/models/accountSettings.js @@ -23,11 +23,17 @@ AccountSettings.allow({ if (Meteor.isServer) { Meteor.startup(() => { - AccountSettings.upsert({ _id: 'accounts-allowEmailChange' }, { + AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, { $setOnInsert: { booleanValue: false, sort: 0, }, }); + AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, { + $setOnInsert: { + booleanValue: false, + sort: 1, + }, + }); }); } diff --git a/models/activities.js b/models/activities.js index 237283f8..f64b53f8 100644 --- a/models/activities.js +++ b/models/activities.js @@ -23,6 +23,9 @@ Activities.helpers({ list() { return Lists.findOne(this.listId); }, + swimlane() { + return Swimlanes.findOne(this.swimlaneId); + }, oldList() { return Lists.findOne(this.oldListId); }, @@ -39,7 +42,7 @@ Activities.helpers({ return Checklists.findOne(this.checklistId); }, checklistItem() { - return Checklists.findOne(this.checklistId).getItem(this.checklistItemId); + return ChecklistItems.findOne(this.checklistItemId); }, customField() { return CustomFields.findOne(this.customFieldId); @@ -114,6 +117,7 @@ if (Meteor.isServer) { if (activity.commentId) { const comment = activity.comment(); params.comment = comment.text; + params.commentId = comment._id; } if (activity.attachmentId) { const attachment = activity.attachment(); diff --git a/models/announcements.js b/models/announcements.js new file mode 100644 index 00000000..2cb1e1b7 --- /dev/null +++ b/models/announcements.js @@ -0,0 +1,36 @@ +Announcements = new Mongo.Collection('announcements'); + +Announcements.attachSchema(new SimpleSchema({ + enabled: { + type: Boolean, + defaultValue: false, + }, + title: { + type: String, + optional: true, + }, + body: { + type: String, + optional: true, + }, + sort: { + type: Number, + decimal: true, + }, +})); + +Announcements.allow({ + update(userId) { + const user = Users.findOne(userId); + return user && user.isAdmin; + }, +}); + +if (Meteor.isServer) { + Meteor.startup(() => { + const announcements = Announcements.findOne({}); + if(!announcements){ + Announcements.insert({enabled: false, sort: 0}); + } + }); +} diff --git a/models/attachments.js b/models/attachments.js index 560bec99..5e5c4926 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,83 +1,90 @@ -Attachments = new FS.Collection('attachments', { - stores: [ + Attachments = new FS.Collection('attachments', { + stores: [ - // XXX Add a new store for cover thumbnails so we don't load big images in - // the general board view - new FS.Store.GridFS('attachments', { - // If the uploaded document is not an image we need to enforce browser - // download instead of execution. This is particularly important for HTML - // files that the browser will just execute if we don't serve them with the - // appropriate `application/octet-stream` MIME header which can lead to user - // data leaks. I imagine other formats (like PDF) can also be attack vectors. - // See https://github.com/wekan/wekan/issues/99 - // XXX Should we use `beforeWrite` option of CollectionFS instead of - // collection-hooks? - // We should use `beforeWrite`. - beforeWrite: (fileObj) => { - if (!fileObj.isImage()) { - return { - type: 'application/octet-stream', - }; + // XXX Add a new store for cover thumbnails so we don't load big images in + // the general board view + new FS.Store.GridFS('attachments', { + // If the uploaded document is not an image we need to enforce browser + // download instead of execution. This is particularly important for HTML + // files that the browser will just execute if we don't serve them with the + // appropriate `application/octet-stream` MIME header which can lead to user + // data leaks. I imagine other formats (like PDF) can also be attack vectors. + // See https://github.com/wekan/wekan/issues/99 + // XXX Should we use `beforeWrite` option of CollectionFS instead of + // collection-hooks? + // We should use `beforeWrite`. + beforeWrite: (fileObj) => { + if (!fileObj.isImage()) { + return { + type: 'application/octet-stream', + }; + } + return {}; + }, + }), + ], + }); + + + if (Meteor.isServer) { + Attachments.allow({ + insert(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); + }, + update(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); + }, + remove(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); + }, + // We authorize the attachment download either: + // - if the board is public, everyone (even unconnected) can download it + // - if the board is private, only board members can download it + download(userId, doc) { + const board = Boards.findOne(doc.boardId); + if (board.isPublic()) { + return true; + } else { + return board.hasMember(userId); } - return {}; }, - }), - ], -}); -if (Meteor.isServer) { - Attachments.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - remove(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - // We authorize the attachment download either: - // - if the board is public, everyone (even unconnected) can download it - // - if the board is private, only board members can download it - download(userId, doc) { - const board = Boards.findOne(doc.boardId); - if (board.isPublic()) { - return true; - } else { - return board.hasMember(userId); - } - }, + fetch: ['boardId'], + }); + } - fetch: ['boardId'], - }); -} + // XXX Enforce a schema for the Attachments CollectionFS -// XXX Enforce a schema for the Attachments CollectionFS + if (Meteor.isServer) { + Attachments.files.after.insert((userId, doc) => { + // If the attachment doesn't have a source field + // or its source is different than import + if (!doc.source || doc.source !== 'import') { + // Add activity about adding the attachment + Activities.insert({ + userId, + type: 'card', + activityType: 'addAttachment', + attachmentId: doc._id, + boardId: doc.boardId, + cardId: doc.cardId, + }); + } else { + // Don't add activity about adding the attachment as the activity + // be imported and delete source field + Attachments.update({ + _id: doc._id, + }, { + $unset: { + source: '', + }, + }); + } + }); -if (Meteor.isServer) { - Attachments.files.after.insert((userId, doc) => { - // If the attachment doesn't have a source field - // or its source is different than import - if (!doc.source || doc.source !== 'import') { - // Add activity about adding the attachment - Activities.insert({ - userId, - type: 'card', - activityType: 'addAttachment', + Attachments.files.after.remove((userId, doc) => { + Activities.remove({ attachmentId: doc._id, - boardId: doc.boardId, - cardId: doc.cardId, }); - } else { - // Don't add activity about adding the attachment as the activity - // be imported and delete source field - Attachments.update( {_id: doc._id}, {$unset: { source : '' } } ); - } - }); - - Attachments.files.after.remove((userId, doc) => { - Activities.remove({ - attachmentId: doc._id, }); - }); -} + } diff --git a/models/boards.js b/models/boards.js index 98c0e46d..44ce0b62 100644 --- a/models/boards.js +++ b/models/boards.js @@ -187,6 +187,20 @@ Boards.helpers({ return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); }, + swimlanes() { + return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); + }, + + hasOvertimeCards(){ + const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} ); + return card !== undefined; + }, + + hasSpentTimeCards(){ + const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} ); + return card !== undefined; + }, + activities() { return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 } }); }, @@ -246,6 +260,27 @@ Boards.helpers({ Boards.direct.update(this._id, { $push: { labels: { _id, name, color } } }); return _id; }, + + searchCards(term) { + check(term, Match.OneOf(String, null, undefined)); + + let query = { boardId: this._id }; + const projection = { limit: 10, sort: { createdAt: -1 } }; + + if (term) { + const regex = new RegExp(term, 'i'); + + query = { + boardId: this._id, + $or: [ + { title: regex }, + { description: regex }, + ], + }; + } + + return Cards.find(query, projection); + }, }); Boards.mutations({ @@ -302,6 +337,15 @@ Boards.mutations({ return { $pull: { labels: { _id: labelId } } }; }, + changeOwnership(fromId, toId) { + const memberIndex = this.memberIndex(fromId); + return { + $set: { + [`members.${memberIndex}.userId`]: toId, + }, + }; + }, + addMember(memberId) { const memberIndex = this.memberIndex(memberId); if (memberIndex >= 0) { @@ -560,83 +604,152 @@ if (Meteor.isServer) { //BOARDS REST API if (Meteor.isServer) { - JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res, next) { - Authentication.checkLoggedIn(req.userId); - const paramUserId = req.params.userId; - // A normal user should be able to see their own boards, - // admins can access boards of any user - Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId); - - const data = Boards.find({ - archived: false, - 'members.userId': req.userId, - }, { - sort: ['title'], - }).map(function(board) { - return { - _id: board._id, - title: board.title, - }; - }); + JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) { + try { + Authentication.checkLoggedIn(req.userId); + const paramUserId = req.params.userId; + // A normal user should be able to see their own boards, + // admins can access boards of any user + Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId); + + const data = Boards.find({ + archived: false, + 'members.userId': paramUserId, + }, { + sort: ['title'], + }).map(function(board) { + return { + _id: board._id, + title: board.title, + }; + }); - JsonRoutes.sendResult(res, {code: 200, data}); + JsonRoutes.sendResult(res, {code: 200, data}); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('GET', '/api/boards', function (req, res, next) { - Authentication.checkUserId(req.userId); - JsonRoutes.sendResult(res, { - code: 200, - data: Boards.find({ permission: 'public' }).map(function (doc) { - return { - _id: doc._id, - title: doc.title, - }; - }), - }); + JsonRoutes.add('GET', '/api/boards', function (req, res) { + try { + Authentication.checkUserId(req.userId); + JsonRoutes.sendResult(res, { + code: 200, + data: Boards.find({ permission: 'public' }).map(function (doc) { + return { + _id: doc._id, + title: doc.title, + }; + }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('GET', '/api/boards/:id', function (req, res, next) { - const id = req.params.id; - Authentication.checkBoardAccess( req.userId, id); + JsonRoutes.add('GET', '/api/boards/:id', function (req, res) { + try { + const id = req.params.id; + Authentication.checkBoardAccess(req.userId, id); - JsonRoutes.sendResult(res, { - code: 200, - data: Boards.findOne({ _id: id }), - }); + JsonRoutes.sendResult(res, { + code: 200, + data: Boards.findOne({ _id: id }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('POST', '/api/boards', function (req, res, next) { - Authentication.checkUserId( req.userId); - const id = Boards.insert({ - title: req.body.title, - members: [ - { - userId: req.body.owner, - isAdmin: true, - isActive: true, - isCommentOnly: false, + JsonRoutes.add('POST', '/api/boards', function (req, res) { + try { + Authentication.checkUserId(req.userId); + const id = Boards.insert({ + title: req.body.title, + members: [ + { + userId: req.body.owner, + isAdmin: true, + isActive: true, + isCommentOnly: false, + }, + ], + permission: 'public', + color: 'belize', + }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, }, - ], - permission: 'public', - color: 'belize', - }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, - }); + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } + }); + + JsonRoutes.add('DELETE', '/api/boards/:id', function (req, res) { + try { + Authentication.checkUserId(req.userId); + const id = req.params.id; + Boards.remove({ _id: id }); + JsonRoutes.sendResult(res, { + code: 200, + data:{ + _id: id, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('DELETE', '/api/boards/:id', function (req, res, next) { - Authentication.checkUserId( req.userId); + JsonRoutes.add('PUT', '/api/boards/:id/labels', function (req, res) { + Authentication.checkUserId(req.userId); const id = req.params.id; - Boards.remove({ _id: id }); - JsonRoutes.sendResult(res, { - code: 200, - data:{ - _id: id, - }, - }); + try { + if (req.body.hasOwnProperty('label')) { + const board = Boards.findOne({ _id: id }); + const color = req.body.label.color; + const name = req.body.label.name; + const labelId = Random.id(6); + if (!board.getLabel(name, color)) { + Boards.direct.update({ _id: id }, { $push: { labels: { _id: labelId, name, color } } }); + JsonRoutes.sendResult(res, { + code: 200, + data: labelId, + }); + } else { + JsonRoutes.sendResult(res, { + code: 200, + }); + } + } + } + catch (error) { + JsonRoutes.sendResult(res, { + data: error, + }); + } }); } diff --git a/models/cardComments.js b/models/cardComments.js index 352030f1..b6cb10fa 100644 --- a/models/cardComments.js +++ b/models/cardComments.js @@ -87,66 +87,98 @@ if (Meteor.isServer) { //CARD COMMENT REST API if (Meteor.isServer) { - JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res, next) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const paramCardId = req.params.cardId; - JsonRoutes.sendResult(res, { - code: 200, - data: CardComments.find({ boardId: paramBoardId, cardId: paramCardId}).map(function (doc) { - return { - _id: doc._id, - comment: doc.text, - authorId: doc.userId, - }; - }), - }); + JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const paramCardId = req.params.cardId; + JsonRoutes.sendResult(res, { + code: 200, + data: CardComments.find({ boardId: paramBoardId, cardId: paramCardId}).map(function (doc) { + return { + _id: doc._id, + comment: doc.text, + authorId: doc.userId, + }; + }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res, next) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const paramCommentId = req.params.commentId; - const paramCardId = req.params.cardId; - JsonRoutes.sendResult(res, { - code: 200, - data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }), - }); + JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const paramCommentId = req.params.commentId; + const paramCardId = req.params.cardId; + JsonRoutes.sendResult(res, { + code: 200, + data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res, next) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const paramCardId = req.params.cardId; - const id = CardComments.direct.insert({ - userId: req.body.authorId, - text: req.body.comment, - cardId: paramCardId, - boardId: paramBoardId, - }); + JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const paramCardId = req.params.cardId; + const id = CardComments.direct.insert({ + userId: req.body.authorId, + text: req.body.comment, + cardId: paramCardId, + boardId: paramBoardId, + }); - const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId }); - commentCreation(req.body.authorId, cardComment); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, - }); + const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId }); + commentCreation(req.body.authorId, cardComment); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res, next) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const paramCommentId = req.params.commentId; - const paramCardId = req.params.cardId; - CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: paramCardId, - }, - }); + JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const paramCommentId = req.params.commentId; + const paramCardId = req.params.cardId; + CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramCardId, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); } diff --git a/models/cards.js b/models/cards.js index 17abf430..8b917ee3 100644 --- a/models/cards.js +++ b/models/cards.js @@ -18,9 +18,12 @@ Cards.attachSchema(new SimpleSchema({ listId: { type: String, }, - // The system could work without this `boardId` information (we could deduce - // the board identifier from the card), but it would make the system more - // difficult to manage and less efficient. + swimlaneId: { + type: String, + }, + // The system could work without this `boardId` information (we could deduce + // the board identifier from the card), but it would make the system more + // difficult to manage and less efficient. boardId: { type: String, }, @@ -71,6 +74,10 @@ Cards.attachSchema(new SimpleSchema({ type: [String], optional: true, }, + receivedAt: { + type: Date, + optional: true, + }, startAt: { type: Date, optional: true, @@ -79,8 +86,22 @@ Cards.attachSchema(new SimpleSchema({ type: Date, optional: true, }, - // XXX Should probably be called `authorId`. Is it even needed since we have - // the `members` field? + endAt: { + type: Date, + optional: true, + }, + spentTime: { + type: Number, + decimal: true, + optional: true, + }, + isOvertime: { + type: Boolean, + defaultValue: false, + optional: true, + }, + // XXX Should probably be called `authorId`. Is it even needed since we have + // the `members` field? userId: { type: String, autoValue() { // eslint-disable-line consistent-return @@ -151,13 +172,13 @@ Cards.helpers({ cover() { const cover = Attachments.findOne(this.coverId); - // if we return a cover before it is fully stored, we will get errors when we try to display it - // todo XXX we could return a default "upload pending" image in the meantime? + // if we return a cover before it is fully stored, we will get errors when we try to display it + // todo XXX we could return a default "upload pending" image in the meantime? return cover && cover.url() && cover; }, checklists() { - return Checklists.find({cardId: this._id}, {sort: {createdAt: 1}}); + return Checklists.find({cardId: this._id}, {sort: { sort: 1 } }); }, checklistItemCount() { @@ -219,6 +240,14 @@ Cards.helpers({ cardId: this._id, }); }, + + canBeRestored() { + const list = Lists.findOne({_id: this.listId}); + if(!list.getWipLimit('soft') && list.getWipLimit('enabled') && list.getWipLimit('value') === list.cards().count()){ + return false; + } + return true; + }, }); Cards.mutations({ @@ -238,11 +267,15 @@ Cards.mutations({ return {$set: {description}}; }, - move(listId, sortIndex) { - const mutatedFields = {listId}; - if (sortIndex) { - mutatedFields.sort = sortIndex; - } + move(swimlaneId, listId, sortIndex) { + const list = Lists.findOne(listId); + const mutatedFields = { + swimlaneId, + listId, + boardId: list.boardId, + sort: sortIndex, + }; + return {$set: mutatedFields}; }, @@ -312,6 +345,14 @@ Cards.mutations({ return {$unset: {coverId: ''}}; }, + setReceived(receivedAt) { + return {$set: {receivedAt}}; + }, + + unsetReceived() { + return {$unset: {receivedAt: ''}}; + }, + setStart(startAt) { return {$set: {startAt}}; }, @@ -327,6 +368,26 @@ Cards.mutations({ unsetDue() { return {$unset: {dueAt: ''}}; }, + + setEnd(endAt) { + return {$set: {endAt}}; + }, + + unsetEnd() { + return {$unset: {endAt: ''}}; + }, + + setOvertime(isOvertime) { + return {$set: {isOvertime}}; + }, + + setSpentTime(spentTime) { + return {$set: {spentTime}}; + }, + + unsetSpentTime() { + return {$unset: {spentTime: '', isOvertime: false}}; + }, }); @@ -371,7 +432,7 @@ function cardMembers(userId, doc, fieldNames, modifier) { if (!_.contains(fieldNames, 'members')) return; let memberId; - // Say hello to the new member + // Say hello to the new member if (modifier.$addToSet && modifier.$addToSet.members) { memberId = modifier.$addToSet.members; if (!_.contains(doc.members, memberId)) { @@ -385,10 +446,10 @@ function cardMembers(userId, doc, fieldNames, modifier) { } } - // Say goodbye to the former member + // Say goodbye to the former member if (modifier.$pull && modifier.$pull.members) { memberId = modifier.$pull.members; - // Check that the former member is member of the card + // Check that the former member is member of the card if (_.contains(doc.members, memberId)) { Activities.insert({ userId, @@ -428,8 +489,8 @@ function cardRemover(userId, doc) { if (Meteor.isServer) { - // Cards are often fetched within a board, so we create an index to make these - // queries more efficient. + // Cards are often fetched within a board, so we create an index to make these + // queries more efficient. Meteor.startup(() => { Cards._collection._ensureIndex({boardId: 1, createdAt: -1}); }); @@ -438,31 +499,31 @@ if (Meteor.isServer) { cardCreation(userId, doc); }); - // New activity for card (un)archivage + // New activity for card (un)archivage Cards.after.update((userId, doc, fieldNames) => { cardState(userId, doc, fieldNames); }); - //New activity for card moves + //New activity for card moves Cards.after.update(function (userId, doc, fieldNames) { const oldListId = this.previous.listId; cardMove(userId, doc, fieldNames, oldListId); }); - // Add a new activity if we add or remove a member to the card + // Add a new activity if we add or remove a member to the card Cards.before.update((userId, doc, fieldNames, modifier) => { cardMembers(userId, doc, fieldNames, modifier); }); - // Remove all activities associated with a card if we remove the card - // Remove also card_comments / checklists / attachments + // Remove all activities associated with a card if we remove the card + // Remove also card_comments / checklists / attachments Cards.after.remove((userId, doc) => { cardRemover(userId, doc); }); } //LISTS REST API if (Meteor.isServer) { - JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function (req, res, next) { + JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function (req, res) { const paramBoardId = req.params.boardId; const paramListId = req.params.listId; Authentication.checkBoardAccess(req.userId, paramBoardId); @@ -478,7 +539,7 @@ if (Meteor.isServer) { }); }); - JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res, next) { + JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res) { const paramBoardId = req.params.boardId; const paramListId = req.params.listId; const paramCardId = req.params.cardId; @@ -489,11 +550,12 @@ if (Meteor.isServer) { }); }); - JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function (req, res, next) { + JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function (req, res) { Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const paramListId = req.params.listId; const check = Users.findOne({_id: req.body.authorId}); + const members = req.body.members || [req.body.authorId]; if (typeof check !== 'undefined') { const id = Cards.direct.insert({ title: req.body.title, @@ -501,8 +563,9 @@ if (Meteor.isServer) { listId: paramListId, description: req.body.description, userId: req.body.authorId, + swimlaneId: req.body.swimlaneId, sort: 0, - members: [req.body.authorId], + members, }); JsonRoutes.sendResult(res, { code: 200, @@ -521,7 +584,7 @@ if (Meteor.isServer) { } }); - JsonRoutes.add('PUT', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res, next) { + JsonRoutes.add('PUT', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res) { Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const paramCardId = req.params.cardId; @@ -530,12 +593,12 @@ if (Meteor.isServer) { if (req.body.hasOwnProperty('title')) { const newTitle = req.body.title; Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false}, - {$set: {title: newTitle}}); + {$set: {title: newTitle}}); } if (req.body.hasOwnProperty('listId')) { const newParamListId = req.body.listId; Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false}, - {$set: {listId: newParamListId}}); + {$set: {listId: newParamListId}}); const card = Cards.findOne({_id: paramCardId} ); cardMove(req.body.authorId, card, {fieldName: 'listId'}, paramListId); @@ -544,7 +607,12 @@ if (Meteor.isServer) { if (req.body.hasOwnProperty('description')) { const newDescription = req.body.description; Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false}, - {$set: {description: newDescription}}); + {$set: {description: newDescription}}); + } + if (req.body.hasOwnProperty('labelIds')) { + const newlabelIds = req.body.labelIds; + Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false}, + {$set: {labelIds: newlabelIds}}); } JsonRoutes.sendResult(res, { code: 200, @@ -555,7 +623,7 @@ if (Meteor.isServer) { }); - JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res, next) { + JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res) { Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const paramListId = req.params.listId; diff --git a/models/checklistItems.js b/models/checklistItems.js new file mode 100644 index 00000000..e075eda2 --- /dev/null +++ b/models/checklistItems.js @@ -0,0 +1,145 @@ +ChecklistItems = new Mongo.Collection('checklistItems'); + +ChecklistItems.attachSchema(new SimpleSchema({ + title: { + type: String, + }, + sort: { + type: Number, + decimal: true, + }, + isFinished: { + type: Boolean, + defaultValue: false, + }, + checklistId: { + type: String, + }, + cardId: { + type: String, + }, +})); + +ChecklistItems.allow({ + insert(userId, doc) { + return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId)); + }, + update(userId, doc) { + return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId)); + }, + remove(userId, doc) { + return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId)); + }, + fetch: ['userId', 'cardId'], +}); + +ChecklistItems.before.insert((userId, doc) => { + if (!doc.userId) { + doc.userId = userId; + } +}); + +// Mutations +ChecklistItems.mutations({ + setTitle(title) { + return { $set: { title } }; + }, + toggleItem() { + return { $set: { isFinished: !this.isFinished } }; + }, + move(checklistId, sortIndex) { + const cardId = Checklists.findOne(checklistId).cardId; + const mutatedFields = { + cardId, + checklistId, + sort: sortIndex, + }; + + return {$set: mutatedFields}; + }, +}); + +// Activities helper +function itemCreation(userId, doc) { + const card = Cards.findOne(doc.cardId); + const boardId = card.boardId; + Activities.insert({ + userId, + activityType: 'addChecklistItem', + cardId: doc.cardId, + boardId, + checklistId: doc.checklistId, + checklistItemId: doc._id, + }); +} + +function itemRemover(userId, doc) { + Activities.remove({ + checklistItemId: doc._id, + }); +} + +// Activities +if (Meteor.isServer) { + Meteor.startup(() => { + ChecklistItems._collection._ensureIndex({ checklistId: 1 }); + }); + + ChecklistItems.after.insert((userId, doc) => { + itemCreation(userId, doc); + }); + + ChecklistItems.after.remove((userId, doc) => { + itemRemover(userId, doc); + }); +} + +if (Meteor.isServer) { + JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { + Authentication.checkUserId( req.userId); + const paramItemId = req.params.itemId; + const checklistItem = ChecklistItems.findOne({ _id: paramItemId }); + if (checklistItem) { + JsonRoutes.sendResult(res, { + code: 200, + data: checklistItem, + }); + } else { + JsonRoutes.sendResult(res, { + code: 500, + }); + } + }); + + JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { + Authentication.checkUserId( req.userId); + + const paramItemId = req.params.itemId; + + if (req.body.hasOwnProperty('isFinished')) { + ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}}); + } + if (req.body.hasOwnProperty('title')) { + ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}}); + } + + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramItemId, + }, + }); + }); + + JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { + Authentication.checkUserId( req.userId); + const paramItemId = req.params.itemId; + ChecklistItems.direct.remove({ _id: paramItemId }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramItemId, + }, + }); + }); +} diff --git a/models/checklists.js b/models/checklists.js index 35ef8ae1..c58453ef 100644 --- a/models/checklists.js +++ b/models/checklists.js @@ -6,24 +6,7 @@ Checklists.attachSchema(new SimpleSchema({ }, title: { type: String, - }, - items: { - type: [Object], - defaultValue: [], - }, - 'items.$._id': { - type: String, - }, - 'items.$.title': { - type: String, - }, - 'items.$.sort': { - type: Number, - decimal: true, - }, - 'items.$.isFinished': { - type: Boolean, - defaultValue: false, + defaultValue: 'Checklist', }, finishedAt: { type: Date, @@ -44,41 +27,26 @@ Checklists.attachSchema(new SimpleSchema({ type: Number, decimal: true, }, - newItemIndex: { - type: Number, - decimal: true, - defaultValue: 0, - }, })); -const self = Checklists; - Checklists.helpers({ itemCount() { - return this.items.length; + return ChecklistItems.find({ checklistId: this._id }).count(); }, - getItems() { - return this.items.sort(function (itemA, itemB) { - if (itemA.sort < itemB.sort) { - return -1; - } - if (itemA.sort > itemB.sort) { - return 1; - } - return 0; - }); + items() { + return ChecklistItems.find({ + checklistId: this._id, + }, { sort: ['sort'] }); }, finishedCount() { - return this.items.filter((item) => { - return item.isFinished; - }).length; + return ChecklistItems.find({ + checklistId: this._id, + isFinished: true, + }).count(); }, isFinished() { return 0 !== this.itemCount() && this.itemCount() === this.finishedCount(); }, - getItem(_id) { - return _.findWhere(this.items, { _id }); - }, itemIndex(itemId) { const items = self.findOne({_id : this._id}).items; return _.pluck(items, '_id').indexOf(itemId); @@ -106,82 +74,9 @@ Checklists.before.insert((userId, doc) => { }); Checklists.mutations({ - //for checklist itself setTitle(title) { return { $set: { title } }; }, - //for items in checklist - addItem(title) { - const itemCount = this.itemCount(); - const _id = `${this._id}${this.newItemIndex}`; - return { - $addToSet: { items: { _id, title, isFinished: false, sort: itemCount } }, - $set: { newItemIndex: this.newItemIndex + 1}, - }; - }, - removeItem(itemId) { - return { $pull: { items: { _id: itemId } } }; - }, - editItem(itemId, title) { - if (this.getItem(itemId)) { - const itemIndex = this.itemIndex(itemId); - return { - $set: { - [`items.${itemIndex}.title`]: title, - }, - }; - } - return {}; - }, - finishItem(itemId) { - if (this.getItem(itemId)) { - const itemIndex = this.itemIndex(itemId); - return { - $set: { - [`items.${itemIndex}.isFinished`]: true, - }, - }; - } - return {}; - }, - resumeItem(itemId) { - if (this.getItem(itemId)) { - const itemIndex = this.itemIndex(itemId); - return { - $set: { - [`items.${itemIndex}.isFinished`]: false, - }, - }; - } - return {}; - }, - toggleItem(itemId) { - const item = this.getItem(itemId); - if (item) { - const itemIndex = this.itemIndex(itemId); - return { - $set: { - [`items.${itemIndex}.isFinished`]: !item.isFinished, - }, - }; - } - return {}; - }, - sortItems(itemIDs) { - const validItems = []; - for (const itemID of itemIDs) { - if (this.getItem(itemID)) { - validItems.push(this.itemIndex(itemID)); - } - } - const modifiedValues = {}; - for (let i = 0; i < validItems.length; i++) { - modifiedValues[`items.${validItems[i]}.sort`] = i; - } - return { - $set: modifiedValues, - }; - }, }); if (Meteor.isServer) { @@ -199,30 +94,6 @@ if (Meteor.isServer) { }); }); - //TODO: so there will be no activity for adding item into checklist, maybe will be implemented in the future. - // The future is now - Checklists.after.update((userId, doc, fieldNames, modifier) => { - if (fieldNames.includes('items')) { - if (modifier.$addToSet) { - Activities.insert({ - userId, - activityType: 'addChecklistItem', - cardId: doc.cardId, - boardId: Cards.findOne(doc.cardId).boardId, - checklistId: doc._id, - checklistItemId: modifier.$addToSet.items._id, - }); - } else if (modifier.$pull) { - const activity = Activities.findOne({ - checklistItemId: modifier.$pull.items._id, - }); - if (activity) { - Activities.remove(activity._id); - } - } - } - }); - Checklists.before.remove((userId, doc) => { const activities = Activities.find({ checklistId: doc._id }); if (activities) { @@ -233,64 +104,91 @@ if (Meteor.isServer) { }); } -//CARD COMMENT REST API if (Meteor.isServer) { - JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res, next) { + JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { Authentication.checkUserId( req.userId); const paramCardId = req.params.cardId; - JsonRoutes.sendResult(res, { - code: 200, - data: Checklists.find({ cardId: paramCardId }).map(function (doc) { - return { - _id: doc._id, - title: doc.title, - }; - }), + const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) { + return { + _id: doc._id, + title: doc.title, + }; }); + if (checklists) { + JsonRoutes.sendResult(res, { + code: 200, + data: checklists, + }); + } else { + JsonRoutes.sendResult(res, { + code: 500, + }); + } }); - JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res, next) { + JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { Authentication.checkUserId( req.userId); const paramChecklistId = req.params.checklistId; const paramCardId = req.params.cardId; - JsonRoutes.sendResult(res, { - code: 200, - data: Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }), - }); + const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }); + if (checklist) { + checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) { + return { + _id: doc._id, + title: doc.title, + isFinished: doc.isFinished, + }; + }); + JsonRoutes.sendResult(res, { + code: 200, + data: checklist, + }); + } else { + JsonRoutes.sendResult(res, { + code: 500, + }); + } }); - JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res, next) { + JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { Authentication.checkUserId( req.userId); - const paramCardId = req.params.cardId; - const checklistToSend = {}; - checklistToSend.cardId = paramCardId; - checklistToSend.title = req.body.title; - checklistToSend.items = []; - const id = Checklists.insert(checklistToSend); - const checklist = Checklists.findOne({_id: id}); - req.body.items.forEach(function (item) { - checklist.addItem(item); - }, this); - - - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, + const paramCardId = req.params.cardId; + const id = Checklists.insert({ + title: req.body.title, + cardId: paramCardId, + sort: 0, }); + if (id) { + req.body.items.forEach(function (item, idx) { + ChecklistItems.insert({ + cardId: paramCardId, + checklistId: id, + title: item.title, + sort: idx, + }); + }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + } else { + JsonRoutes.sendResult(res, { + code: 400, + }); + } }); - JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res, next) { + JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { Authentication.checkUserId( req.userId); - const paramCommentId = req.params.commentId; - const paramCardId = req.params.cardId; - Checklists.remove({ _id: paramCommentId, cardId: paramCardId }); + const paramChecklistId = req.params.checklistId; + Checklists.remove({ _id: paramChecklistId }); JsonRoutes.sendResult(res, { code: 200, data: { - _id: paramCardId, + _id: paramChecklistId, }, }); }); diff --git a/models/export.js b/models/export.js index 49656134..aff66801 100644 --- a/models/export.js +++ b/models/export.js @@ -53,12 +53,16 @@ class Exporter { _.extend(result, Boards.findOne(this._boardId, { fields: { stars: 0 } })); result.lists = Lists.find(byBoard, noBoardId).fetch(); result.cards = Cards.find(byBoard, noBoardId).fetch(); + result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); result.comments = CardComments.find(byBoard, noBoardId).fetch(); result.activities = Activities.find(byBoard, noBoardId).fetch(); result.checklists = []; + result.checklistItems = []; result.cards.forEach((card) => { result.checklists.push(...Checklists.find({ cardId: card._id }).fetch()); + result.checklistItems.push(...ChecklistItems.find({ cardId: card._id }).fetch()); }); + // [Old] for attachments we only export IDs and absolute url to original doc // [New] Encode attachment to base64 const getBase64Data = function(doc, callback) { diff --git a/models/integrations.js b/models/integrations.js index 826873ce..1062b93b 100644 --- a/models/integrations.js +++ b/models/integrations.js @@ -59,132 +59,188 @@ Integrations.allow({ //INTEGRATIONS REST API if (Meteor.isServer) { // Get all integrations in board - JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res, next) { - const paramBoardId = req.params.boardId; - Authentication.checkBoardAccess(req.userId, paramBoardId); + JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res) { + try { + const paramBoardId = req.params.boardId; + Authentication.checkBoardAccess(req.userId, paramBoardId); - const data = Integrations.find({ boardId: paramBoardId }, { fields: { token: 0 } }).map(function(doc) { - return doc; - }); + const data = Integrations.find({ boardId: paramBoardId }, { fields: { token: 0 } }).map(function(doc) { + return doc; + }); - JsonRoutes.sendResult(res, {code: 200, data}); + JsonRoutes.sendResult(res, {code: 200, data}); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); // Get a single integration in board - JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res, next) { - const paramBoardId = req.params.boardId; - const paramIntId = req.params.intId; - Authentication.checkBoardAccess(req.userId, paramBoardId); - - JsonRoutes.sendResult(res, { - code: 200, - data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }), - }); + JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res) { + try { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + JsonRoutes.sendResult(res, { + code: 200, + data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); // Create a new integration - JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res, next) { - const paramBoardId = req.params.boardId; - Authentication.checkBoardAccess(req.userId, paramBoardId); - - const id = Integrations.insert({ - userId: req.userId, - boardId: paramBoardId, - url: req.body.url, - }); - - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, - }); + JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res) { + try { + const paramBoardId = req.params.boardId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + const id = Integrations.insert({ + userId: req.userId, + boardId: paramBoardId, + url: req.body.url, + }); + + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); // Edit integration data - JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res, next) { - const paramBoardId = req.params.boardId; - const paramIntId = req.params.intId; - Authentication.checkBoardAccess(req.userId, paramBoardId); + JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res) { + try { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + if (req.body.hasOwnProperty('enabled')) { + const newEnabled = req.body.enabled; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {enabled: newEnabled}}); + } + if (req.body.hasOwnProperty('title')) { + const newTitle = req.body.title; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {title: newTitle}}); + } + if (req.body.hasOwnProperty('url')) { + const newUrl = req.body.url; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {url: newUrl}}); + } + if (req.body.hasOwnProperty('token')) { + const newToken = req.body.token; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {token: newToken}}); + } + if (req.body.hasOwnProperty('activities')) { + const newActivities = req.body.activities; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {activities: newActivities}}); + } - if (req.body.hasOwnProperty('enabled')) { - const newEnabled = req.body.enabled; - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$set: {enabled: newEnabled}}); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramIntId, + }, + }); } - if (req.body.hasOwnProperty('title')) { - const newTitle = req.body.title; - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$set: {title: newTitle}}); + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); } - if (req.body.hasOwnProperty('url')) { - const newUrl = req.body.url; - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$set: {url: newUrl}}); - } - if (req.body.hasOwnProperty('token')) { - const newToken = req.body.token; - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$set: {token: newToken}}); - } - if (req.body.hasOwnProperty('activities')) { - const newActivities = req.body.activities; - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$set: {activities: newActivities}}); - } - - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: paramIntId, - }, - }); }); // Delete subscribed activities - JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res, next) { - const paramBoardId = req.params.boardId; - const paramIntId = req.params.intId; - const newActivities = req.body.activities; - Authentication.checkBoardAccess(req.userId, paramBoardId); - - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$pullAll: {activities: newActivities}}); - - JsonRoutes.sendResult(res, { - code: 200, - data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), - }); + JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) { + try { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + const newActivities = req.body.activities; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$pullAll: {activities: newActivities}}); + + JsonRoutes.sendResult(res, { + code: 200, + data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); // Add subscribed activities - JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res, next) { - const paramBoardId = req.params.boardId; - const paramIntId = req.params.intId; - const newActivities = req.body.activities; - Authentication.checkBoardAccess(req.userId, paramBoardId); - - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$addToSet: {activities: { $each: newActivities}}}); - - JsonRoutes.sendResult(res, { - code: 200, - data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), - }); + JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) { + try { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + const newActivities = req.body.activities; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$addToSet: {activities: { $each: newActivities}}}); + + JsonRoutes.sendResult(res, { + code: 200, + data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); // Delete integration - JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res, next) { - const paramBoardId = req.params.boardId; - const paramIntId = req.params.intId; - Authentication.checkBoardAccess(req.userId, paramBoardId); - - Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId}); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: paramIntId, - }, - }); + JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res) { + try { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId}); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramIntId, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); } diff --git a/models/invitationCodes.js b/models/invitationCodes.js index 5761977a..53163f06 100644 --- a/models/invitationCodes.js +++ b/models/invitationCodes.js @@ -34,8 +34,8 @@ InvitationCodes.helpers({ }); // InvitationCodes.before.insert((userId, doc) => { - // doc.createdAt = new Date(); - // doc.authorId = userId; +// doc.createdAt = new Date(); +// doc.authorId = userId; // }); if (Meteor.isServer) { diff --git a/models/lists.js b/models/lists.js index d9a5b8e2..6f6996cb 100644 --- a/models/lists.js +++ b/models/lists.js @@ -42,6 +42,23 @@ Lists.attachSchema(new SimpleSchema({ } }, }, + wipLimit: { + type: Object, + optional: true, + }, + 'wipLimit.value': { + type: Number, + decimal: false, + defaultValue: 1, + }, + 'wipLimit.enabled': { + type: Boolean, + defaultValue: false, + }, + 'wipLimit.soft': { + type: Boolean, + defaultValue: false, + }, })); Lists.allow({ @@ -58,11 +75,15 @@ Lists.allow({ }); Lists.helpers({ - cards() { - return Cards.find(Filter.mongoSelector({ + cards(swimlaneId) { + const selector = { listId: this._id, archived: false, - }), { sort: ['sort'] }); + }; + if (swimlaneId) + selector.swimlaneId = swimlaneId; + return Cards.find(Filter.mongoSelector(selector), + { sort: ['sort'] }); }, allCards() { @@ -72,6 +93,17 @@ Lists.helpers({ board() { return Boards.findOne(this.boardId); }, + + getWipLimit(option){ + const list = Lists.findOne({ _id: this._id }); + if(!list.wipLimit) { // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set + return 0; + } else if(!option) { + return list.wipLimit; + } else { + return list.wipLimit[option] ? list.wipLimit[option] : 0; // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set + } + }, }); Lists.mutations({ @@ -86,6 +118,44 @@ Lists.mutations({ restore() { return { $set: { archived: false } }; }, + + toggleSoftLimit(toggle) { + return { $set: { 'wipLimit.soft': toggle } }; + }, + + toggleWipLimit(toggle) { + return { $set: { 'wipLimit.enabled': toggle } }; + }, + + setWipLimit(limit) { + return { $set: { 'wipLimit.value': limit } }; + }, +}); + +Meteor.methods({ + applyWipLimit(listId, limit){ + check(listId, String); + check(limit, Number); + if(limit === 0){ + limit = 1; + } + Lists.findOne({ _id: listId }).setWipLimit(limit); + }, + + enableWipLimit(listId) { + check(listId, String); + const list = Lists.findOne({ _id: listId }); + if(list.getWipLimit('value') === 0){ + list.setWipLimit(1); + } + list.toggleWipLimit(!list.getWipLimit('enabled')); + }, + + enableSoftLimit(listId) { + check(listId, String); + const list = Lists.findOne({ _id: listId }); + list.toggleSoftLimit(!list.getWipLimit('soft')); + }, }); Lists.hookOptions.after.update = { fetchPrevious: false }; @@ -131,57 +201,89 @@ if (Meteor.isServer) { //LISTS REST API if (Meteor.isServer) { - JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res, next) { - const paramBoardId = req.params.boardId; - Authentication.checkBoardAccess( req.userId, paramBoardId); - - JsonRoutes.sendResult(res, { - code: 200, - data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) { - return { - _id: doc._id, - title: doc.title, - }; - }), - }); + JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) { + try { + const paramBoardId = req.params.boardId; + Authentication.checkBoardAccess( req.userId, paramBoardId); + + JsonRoutes.sendResult(res, { + code: 200, + data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) { + return { + _id: doc._id, + title: doc.title, + }; + }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res, next) { - const paramBoardId = req.params.boardId; - const paramListId = req.params.listId; - Authentication.checkBoardAccess( req.userId, paramBoardId); - JsonRoutes.sendResult(res, { - code: 200, - data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }), - }); + JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res) { + try { + const paramBoardId = req.params.boardId; + const paramListId = req.params.listId; + Authentication.checkBoardAccess( req.userId, paramBoardId); + JsonRoutes.sendResult(res, { + code: 200, + data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res, next) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const id = Lists.insert({ - title: req.body.title, - boardId: paramBoardId, - }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, - }); + JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const id = Lists.insert({ + title: req.body.title, + boardId: paramBoardId, + }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res, next) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const paramListId = req.params.listId; - Lists.remove({ _id: paramListId, boardId: paramBoardId }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: paramListId, - }, - }); + JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const paramListId = req.params.listId; + Lists.remove({ _id: paramListId, boardId: paramBoardId }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramListId, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); } diff --git a/models/settings.js b/models/settings.js index a490d9c5..34f693d9 100644 --- a/models/settings.js +++ b/models/settings.js @@ -105,7 +105,7 @@ if (Meteor.isServer) { inviter: Users.findOne(icode.authorId).username, user: icode.email.split('@')[0], icode: icode.code, - url: FlowRouter.url('sign-in'), + url: FlowRouter.url('sign-up'), }; const lang = author.getLanguage(); Email.send({ @@ -141,5 +141,31 @@ if (Meteor.isServer) { } }); }, + + sendSMTPTestEmail() { + if (!Meteor.userId()) { + throw new Meteor.Error('invalid-user'); + } + const user = Meteor.user(); + if (!user.emails && !user.emails[0] && user.emails[0].address) { + throw new Meteor.Error('email-invalid'); + } + this.unblock(); + const lang = user.getLanguage(); + try { + Email.send({ + to: user.emails[0].address, + from: Accounts.emailTemplates.from, + subject: TAPi18n.__('email-smtp-test-subject', {lng: lang}), + text: TAPi18n.__('email-smtp-test-text', {lng: lang}), + }); + } catch ({message}) { + throw new Meteor.Error('email-fail', `${TAPi18n.__('email-fail-text', {lng: lang})}: ${ message }`, message); + } + return { + message: 'email-sent', + email: user.emails[0].address, + }; + }, }); } diff --git a/models/swimlanes.js b/models/swimlanes.js new file mode 100644 index 00000000..72ef3f36 --- /dev/null +++ b/models/swimlanes.js @@ -0,0 +1,219 @@ +Swimlanes = new Mongo.Collection('swimlanes'); + +Swimlanes.attachSchema(new SimpleSchema({ + title: { + type: String, + }, + archived: { + type: Boolean, + autoValue() { // eslint-disable-line consistent-return + if (this.isInsert && !this.isSet) { + return false; + } + }, + }, + boardId: { + type: String, + }, + createdAt: { + type: Date, + autoValue() { // eslint-disable-line consistent-return + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + sort: { + type: Number, + decimal: true, + // XXX We should probably provide a default + optional: true, + }, + updatedAt: { + type: Date, + optional: true, + autoValue() { // eslint-disable-line consistent-return + if (this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, +})); + +Swimlanes.allow({ + insert(userId, doc) { + return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId)); + }, + update(userId, doc) { + return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId)); + }, + remove(userId, doc) { + return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId)); + }, + fetch: ['boardId'], +}); + +Swimlanes.helpers({ + cards() { + return Cards.find(Filter.mongoSelector({ + swimlaneId: this._id, + archived: false, + }), { sort: ['sort'] }); + }, + + allCards() { + return Cards.find({ swimlaneId: this._id }); + }, + + board() { + return Boards.findOne(this.boardId); + }, +}); + +Swimlanes.mutations({ + rename(title) { + return { $set: { title } }; + }, + + archive() { + return { $set: { archived: true } }; + }, + + restore() { + return { $set: { archived: false } }; + }, +}); + +Swimlanes.hookOptions.after.update = { fetchPrevious: false }; + +if (Meteor.isServer) { + Meteor.startup(() => { + Swimlanes._collection._ensureIndex({ boardId: 1 }); + }); + + Swimlanes.after.insert((userId, doc) => { + Activities.insert({ + userId, + type: 'swimlane', + activityType: 'createSwimlane', + boardId: doc.boardId, + swimlaneId: doc._id, + }); + }); + + Swimlanes.before.remove((userId, doc) => { + Activities.insert({ + userId, + type: 'swimlane', + activityType: 'removeSwimlane', + boardId: doc.boardId, + swimlaneId: doc._id, + title: doc.title, + }); + }); + + Swimlanes.after.update((userId, doc) => { + if (doc.archived) { + Activities.insert({ + userId, + type: 'swimlane', + activityType: 'archivedSwimlane', + swimlaneId: doc._id, + boardId: doc.boardId, + }); + } + }); +} + +//SWIMLANE REST API +if (Meteor.isServer) { + JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) { + try { + const paramBoardId = req.params.boardId; + Authentication.checkBoardAccess( req.userId, paramBoardId); + + JsonRoutes.sendResult(res, { + code: 200, + data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) { + return { + _id: doc._id, + title: doc.title, + }; + }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } + }); + + JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) { + try { + const paramBoardId = req.params.boardId; + const paramSwimlaneId = req.params.swimlaneId; + Authentication.checkBoardAccess( req.userId, paramBoardId); + JsonRoutes.sendResult(res, { + code: 200, + data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } + }); + + JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const id = Swimlanes.insert({ + title: req.body.title, + boardId: paramBoardId, + }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } + }); + + JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) { + try { + Authentication.checkUserId( req.userId); + const paramBoardId = req.params.boardId; + const paramSwimlaneId = req.params.swimlaneId; + Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramSwimlaneId, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } + }); + +} diff --git a/models/trelloCreator.js b/models/trelloCreator.js index e7f98e85..30f0bc2b 100644 --- a/models/trelloCreator.js +++ b/models/trelloCreator.js @@ -23,6 +23,8 @@ export class TrelloCreator { // Map of labels Trello ID => Wekan ID this.labels = {}; + // Default swimlane + this.swimlane = null; // Map of lists Trello ID => Wekan ID this.lists = {}; // Map of cards Trello ID => Wekan ID @@ -113,7 +115,6 @@ export class TrelloCreator { check(trelloLabels, [Match.ObjectIncluding({ // XXX refine control by validating 'color' against a list of allowed // values (is it worth the maintenance?) - color: String, name: String, })]); } @@ -150,6 +151,7 @@ export class TrelloCreator { isAdmin: true, isActive: true, isCommentOnly: false, + swimlaneId: false, }], permission: this.getPermission(trelloBoard.prefs.permissionLevel), slug: getSlug(trelloBoard.name) || 'board', @@ -176,6 +178,7 @@ export class TrelloCreator { isAdmin: this.getAdmin(trelloMembership.memberType), isActive: true, isCommentOnly: false, + swimlaneId: false, }); } } @@ -184,7 +187,7 @@ export class TrelloCreator { trelloBoard.labels.forEach((label) => { const labelToCreate = { _id: Random.id(6), - color: label.color, + color: label.color ? label.color : 'black', name: label.name, }; // We need to remember them by Trello ID, as this is the only ref we have @@ -229,6 +232,7 @@ export class TrelloCreator { dateLastActivity: this._now(), description: card.desc, listId: this.lists[card.idList], + swimlaneId: this.swimlane, sort: card.pos, title: card.name, // we attribute the card to its creator if available @@ -375,6 +379,7 @@ export class TrelloCreator { // we require. createdAt: this._now(this.createdAt.lists[list.id]), title: list.name, + sort: list.pos, }; const listId = Lists.direct.insert(listToCreate); Lists.direct.update(listId, {$set: {'updatedAt': this._now()}}); @@ -396,29 +401,51 @@ export class TrelloCreator { }); } + createSwimlanes(boardId) { + const swimlaneToCreate = { + archived: false, + boardId, + // We are being defensing here by providing a default date (now) if the + // creation date wasn't found on the action log. This happen on old + // Wekan boards (eg from 2013) that didn't log the 'createList' action + // we require. + createdAt: this._now(), + title: 'Default', + sort: 1, + }; + const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate); + Swimlanes.direct.update(swimlaneId, {$set: {'updatedAt': this._now()}}); + this.swimlane = swimlaneId; + } + createChecklists(trelloChecklists) { trelloChecklists.forEach((checklist) => { - // Create the checklist - const checklistToCreate = { - cardId: this.cards[checklist.idCard], - title: checklist.name, - createdAt: this._now(), - sort: checklist.pos, - }; - const checklistId = Checklists.direct.insert(checklistToCreate); - // keep track of Trello id => WeKan id - this.checklists[checklist.id] = checklistId; - // Now add the items to the checklist - const itemsToCreate = []; - checklist.checkItems.forEach((item) => { - itemsToCreate.push({ - _id: checklistId + itemsToCreate.length, - title: item.name, - isFinished: item.state === 'complete', - sort: item.pos, + if (this.cards[checklist.idCard]) { + // Create the checklist + const checklistToCreate = { + cardId: this.cards[checklist.idCard], + title: checklist.name, + createdAt: this._now(), + sort: checklist.pos, + }; + const checklistId = Checklists.direct.insert(checklistToCreate); + // keep track of Trello id => WeKan id + this.checklists[checklist.id] = checklistId; + // Now add the items to the checklistItems + let counter = 0; + checklist.checkItems.forEach((item) => { + counter++; + const checklistItemTocreate = { + _id: checklistId + counter, + title: item.name, + checklistId: this.checklists[checklist.id], + cardId: this.cards[checklist.idCard], + sort: item.pos, + isFinished: item.state === 'complete', + }; + ChecklistItems.direct.insert(checklistItemTocreate); }); - }); - Checklists.direct.update(checklistId, {$set: {items: itemsToCreate}}); + } }); } @@ -604,6 +631,7 @@ export class TrelloCreator { this.parseActions(board.actions); const boardId = this.createBoardAndLabels(board); this.createLists(board.lists, boardId); + this.createSwimlanes(boardId); this.createCards(board.cards, boardId); this.createChecklists(board.checklists); this.importActions(board.actions, boardId); diff --git a/models/users.js b/models/users.js index c2238cde..0093f7cb 100644 --- a/models/users.js +++ b/models/users.js @@ -43,7 +43,9 @@ Users.attachSchema(new SimpleSchema({ optional: true, autoValue() { // eslint-disable-line consistent-return if (this.isInsert && !this.isSet) { - return {}; + return { + boardView: 'board-view-lists', + }; } }, }, @@ -95,6 +97,10 @@ Users.attachSchema(new SimpleSchema({ type: String, optional: true, }, + 'profile.boardView': { + type: String, + optional: true, + }, services: { type: Object, optional: true, @@ -108,8 +114,23 @@ Users.attachSchema(new SimpleSchema({ type: Boolean, optional: true, }, + createdThroughApi: { + type: Boolean, + optional: true, + }, + loginDisabled: { + type: Boolean, + optional: true, + }, })); +Users.allow({ + update(userId) { + const user = Users.findOne(userId); + return user && Meteor.user().isAdmin; + }, +}); + // Search a user in the complete server database by its name or username. This // is used for instance to add a new user to a board. const searchInFields = ['username', 'profile.fullname']; @@ -144,36 +165,36 @@ if (Meteor.isClient) { Users.helpers({ boards() { - return Boards.find({ userId: this._id }); + return Boards.find({ 'members.userId': this._id }); }, starredBoards() { - const { starredBoards = [] } = this.profile; - return Boards.find({ archived: false, _id: { $in: starredBoards } }); + const {starredBoards = []} = this.profile; + return Boards.find({archived: false, _id: {$in: starredBoards}}); }, hasStarred(boardId) { - const { starredBoards = [] } = this.profile; + const {starredBoards = []} = this.profile; return _.contains(starredBoards, boardId); }, invitedBoards() { - const { invitedBoards = [] } = this.profile; - return Boards.find({ archived: false, _id: { $in: invitedBoards } }); + const {invitedBoards = []} = this.profile; + return Boards.find({archived: false, _id: {$in: invitedBoards}}); }, isInvitedTo(boardId) { - const { invitedBoards = [] } = this.profile; + const {invitedBoards = []} = this.profile; return _.contains(invitedBoards, boardId); }, hasTag(tag) { - const { tags = [] } = this.profile; + const {tags = []} = this.profile; return _.contains(tags, tag); }, hasNotification(activityId) { - const { notifications = [] } = this.profile; + const {notifications = []} = this.profile; return _.contains(notifications, activityId); }, @@ -183,7 +204,7 @@ Users.helpers({ }, getEmailBuffer() { - const { emailBuffer = [] } = this.profile; + const {emailBuffer = []} = this.profile; return emailBuffer; }, @@ -308,22 +329,30 @@ Users.mutations({ }, setAvatarUrl(avatarUrl) { - return { $set: { 'profile.avatarUrl': avatarUrl } }; + return {$set: {'profile.avatarUrl': avatarUrl}}; }, setShowCardsCountAt(limit) { - return { $set: { 'profile.showCardsCountAt': limit } }; + return {$set: {'profile.showCardsCountAt': limit}}; + }, + + setBoardView(view) { + return { + $set : { + 'profile.boardView': view, + }, + }; }, }); Meteor.methods({ - setUsername(username) { + setUsername(username, userId) { check(username, String); - const nUsersWithUsername = Users.find({ username }).count(); + const nUsersWithUsername = Users.find({username}).count(); if (nUsersWithUsername > 0) { throw new Meteor.Error('username-already-taken'); } else { - Users.update(this.userId, { $set: { username } }); + Users.update(userId, {$set: {username}}); } }, toggleSystemMessages() { @@ -334,13 +363,13 @@ Meteor.methods({ check(limit, Number); Meteor.user().setShowCardsCountAt(limit); }, - setEmail(email) { + setEmail(email, userId) { check(email, String); - const existingUser = Users.findOne({ 'emails.address': email }, { fields: { _id: 1 } }); + const existingUser = Users.findOne({'emails.address': email}, {fields: {_id: 1}}); if (existingUser) { throw new Meteor.Error('email-already-taken'); } else { - Users.update(this.userId, { + Users.update(userId, { $set: { emails: [{ address: email, @@ -350,11 +379,19 @@ Meteor.methods({ }); } }, - setUsernameAndEmail(username, email) { + setUsernameAndEmail(username, email, userId) { check(username, String); check(email, String); - Meteor.call('setUsername', username); - Meteor.call('setEmail', email); + check(userId, String); + Meteor.call('setUsername', username, userId); + Meteor.call('setEmail', email, userId); + }, + setPassword(newPassword, userId) { + check(userId, String); + check(newPassword, String); + if(Meteor.user().isAdmin){ + Accounts.setPassword(userId, newPassword); + } }, }); @@ -371,8 +408,8 @@ if (Meteor.isServer) { board && board.members && _.contains(_.pluck(board.members, 'userId'), inviter._id) && - _.where(board.members, { userId: inviter._id })[0].isActive && - _.where(board.members, { userId: inviter._id })[0].isAdmin; + _.where(board.members, {userId: inviter._id})[0].isActive && + _.where(board.members, {userId: inviter._id})[0].isAdmin; if (!allowInvite) throw new Meteor.Error('error-board-notAMember'); this.unblock(); @@ -380,9 +417,9 @@ if (Meteor.isServer) { const posAt = username.indexOf('@'); let user = null; if (posAt >= 0) { - user = Users.findOne({ emails: { $elemMatch: { address: username } } }); + user = Users.findOne({emails: {$elemMatch: {address: username}}}); } else { - user = Users.findOne(username) || Users.findOne({ username }); + user = Users.findOne(username) || Users.findOne({username}); } if (user) { if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf'); @@ -392,7 +429,7 @@ if (Meteor.isServer) { // Set in lowercase email before creating account const email = username.toLowerCase(); username = email.substring(0, posAt); - const newUserId = Accounts.createUser({ username, email }); + const newUserId = Accounts.createUser({username, email}); if (!newUserId) throw new Meteor.Error('error-user-notCreated'); // assume new user speak same language with inviter if (inviter.profile && inviter.profile.language) { @@ -426,7 +463,7 @@ if (Meteor.isServer) { } catch (e) { throw new Meteor.Error('email-fail', e.message); } - return { username: user.username, email: user.emails[0].address }; + return {username: user.username, email: user.emails[0].address}; }, }); Accounts.onCreateUser((options, user) => { @@ -435,6 +472,12 @@ if (Meteor.isServer) { user.isAdmin = true; return user; } + + if (options.from === 'admin') { + user.createdThroughApi = true; + return user; + } + const disableRegistration = Settings.findOne().disableRegistration; if (!disableRegistration) { return user; @@ -443,11 +486,16 @@ if (Meteor.isServer) { if (!options || !options.profile) { throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required'); } - const invitationCode = InvitationCodes.findOne({ code: options.profile.invitationcode, email: options.email, valid: true }); + const invitationCode = InvitationCodes.findOne({ + code: options.profile.invitationcode, + email: options.email, + valid: true, + }); if (!invitationCode) { throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist'); } else { - user.profile = { icode: options.profile.invitationcode }; + user.profile = {icode: options.profile.invitationcode}; + user.profile.boardView = 'board-view-lists'; } return user; @@ -459,7 +507,7 @@ if (Meteor.isServer) { Meteor.startup(() => { Users._collection._ensureIndex({ username: 1, - }, { unique: true }); + }, {unique: true}); }); // Each board document contains the de-normalized number of users that have @@ -478,6 +526,7 @@ if (Meteor.isServer) { function getStarredBoardsIds(doc) { return doc.profile && doc.profile.starredBoards; } + const oldIds = getStarredBoardsIds(this.previous); const newIds = getStarredBoardsIds(user); @@ -486,9 +535,10 @@ if (Meteor.isServer) { // direction and then in the other. function incrementBoards(boardsIds, inc) { boardsIds.forEach((boardId) => { - Boards.update(boardId, { $inc: { stars: inc } }); + Boards.update(boardId, {$inc: {stars: inc}}); }); } + incrementBoards(_.difference(oldIds, newIds), -1); incrementBoards(_.difference(newIds, oldIds), +1); }); @@ -514,8 +564,14 @@ if (Meteor.isServer) { permission: 'private', }, fakeUser, (err, boardId) => { - ['welcome-list1', 'welcome-list2'].forEach((title) => { - Lists.insert({ title: TAPi18n.__(title), boardId }, fakeUser); + Swimlanes.insert({ + title: TAPi18n.__('welcome-swimlane'), + boardId, + sort: 1, + }, fakeUser); + + ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => { + Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser); }); }); }); @@ -524,10 +580,21 @@ if (Meteor.isServer) { Users.after.insert((userId, doc) => { + if (doc.createdThroughApi) { + // The admin user should be able to create a user despite disabling registration because + // it is two different things (registration and creation). + // So, when a new user is created via the api (only admin user can do that) one must avoid + // the disableRegistration check. + // Issue : https://github.com/wekan/wekan/issues/1232 + // PR : https://github.com/wekan/wekan/pull/1251 + Users.update(doc._id, {$set: {createdThroughApi: ''}}); + return; + } + //invite user to corresponding boards const disableRegistration = Settings.findOne().disableRegistration; if (disableRegistration) { - const invitationCode = InvitationCodes.findOne({ code: doc.profile.icode, valid: true }); + const invitationCode = InvitationCodes.findOne({code: doc.profile.icode, valid: true}); if (!invitationCode) { throw new Meteor.Error('error-invitation-code-not-exist'); } else { @@ -539,8 +606,8 @@ if (Meteor.isServer) { doc.profile = {}; } doc.profile.invitedBoards = invitationCode.boardsToBeInvited; - Users.update(doc._id, { $set: { profile: doc.profile } }); - InvitationCodes.update(invitationCode._id, { $set: { valid: false } }); + Users.update(doc._id, {$set: {profile: doc.profile}}); + InvitationCodes.update(invitationCode._id, {$set: {valid: false}}); } } }); @@ -549,59 +616,143 @@ if (Meteor.isServer) { // USERS REST API if (Meteor.isServer) { - JsonRoutes.add('GET', '/api/user', function(req, res, next) { - Authentication.checkLoggedIn(req.userId); - const data = Meteor.users.findOne({ _id: req.userId}); - delete data.services; - JsonRoutes.sendResult(res, { - code: 200, - data, - }); + JsonRoutes.add('GET', '/api/user', function(req, res) { + try { + Authentication.checkLoggedIn(req.userId); + const data = Meteor.users.findOne({ _id: req.userId}); + delete data.services; + JsonRoutes.sendResult(res, { + code: 200, + data, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('GET', '/api/users', function (req, res, next) { - Authentication.checkUserId( req.userId); - JsonRoutes.sendResult(res, { - code: 200, - data: Meteor.users.find({}).map(function (doc) { - return { _id: doc._id, username: doc.username }; - }), - }); + JsonRoutes.add('GET', '/api/users', function (req, res) { + try { + Authentication.checkUserId(req.userId); + JsonRoutes.sendResult(res, { + code: 200, + data: Meteor.users.find({}).map(function (doc) { + return { _id: doc._id, username: doc.username }; + }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('GET', '/api/users/:id', function (req, res, next) { - Authentication.checkUserId( req.userId); - const id = req.params.id; - JsonRoutes.sendResult(res, { - code: 200, - data: Meteor.users.findOne({ _id: id }), - }); + + JsonRoutes.add('GET', '/api/users/:id', function (req, res) { + try { + Authentication.checkUserId(req.userId); + const id = req.params.id; + JsonRoutes.sendResult(res, { + code: 200, + data: Meteor.users.findOne({ _id: id }), + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('POST', '/api/users/', function (req, res, next) { - Authentication.checkUserId( req.userId); - const id = Accounts.createUser({ - username: req.body.username, - email: req.body.email, - password: 'default', - }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, - }); + JsonRoutes.add('PUT', '/api/users/:id', function (req, res) { + try { + Authentication.checkUserId(req.userId); + const id = req.params.id; + const action = req.body.action; + let data = Meteor.users.findOne({ _id: id }); + if (data !== undefined) { + if (action === 'takeOwnership') { + data = Boards.find({ + 'members.userId': id, + 'members.isAdmin': true, + }).map(function(board) { + if (board.hasMember(req.userId)) { + board.removeMember(req.userId); + } + board.changeOwnership(id, req.userId); + return { + _id: board._id, + title: board.title, + }; + }); + } else { + if ((action === 'disableLogin') && (id !== req.userId)) { + Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } }); + } else if (action === 'enableLogin') { + Users.update({ _id: id }, { $set: { loginDisabled: '' } }); + } + data = Meteor.users.findOne({ _id: id }); + } + } + JsonRoutes.sendResult(res, { + code: 200, + data, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); - JsonRoutes.add('DELETE', '/api/users/:id', function (req, res, next) { - Authentication.checkUserId( req.userId); - const id = req.params.id; - Meteor.users.remove({ _id: id }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, - }); + JsonRoutes.add('POST', '/api/users/', function (req, res) { + try { + Authentication.checkUserId(req.userId); + const id = Accounts.createUser({ + username: req.body.username, + email: req.body.email, + password: req.body.password, + from: 'admin', + }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } }); -} + JsonRoutes.add('DELETE', '/api/users/:id', function (req, res) { + try { + Authentication.checkUserId(req.userId); + const id = req.params.id; + Meteor.users.remove({ _id: id }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } + }); +} diff --git a/models/wekanCreator.js b/models/wekanCreator.js index 3cd65fd7..4551979b 100644 --- a/models/wekanCreator.js +++ b/models/wekanCreator.js @@ -14,6 +14,7 @@ export class WekanCreator { board: null, cards: {}, lists: {}, + swimlanes: {}, }; // The object creator Wekan Id, indexed by the object Wekan id // (so we only parse actions once!) @@ -23,6 +24,8 @@ export class WekanCreator { // Map of labels Wekan ID => Wekan ID this.labels = {}; + // Map of swimlanes Wekan ID => Wekan ID + this.swimlanes = {}; // Map of lists Wekan ID => Wekan ID this.lists = {}; // Map of cards Wekan ID => Wekan ID @@ -33,6 +36,8 @@ export class WekanCreator { this.attachmentIds = {}; // Map of checklists Wekan ID => Wekan ID this.checklists = {}; + // Map of checklistItems Wekan ID => Wekan ID + this.checklistItems = {}; // The comments, indexed by Wekan card id (to map when importing cards) this.comments = {}; // the members, indexed by Wekan member id => Wekan user ID @@ -121,59 +126,62 @@ export class WekanCreator { })]); } + checkSwimlanes(wekanSwimlanes) { + check(wekanSwimlanes, [Match.ObjectIncluding({ + archived: Boolean, + title: String, + })]); + } + checkChecklists(wekanChecklists) { check(wekanChecklists, [Match.ObjectIncluding({ cardId: String, title: String, - items: [Match.ObjectIncluding({ - isFinished: Boolean, - title: String, - })], + })]); + } + + checkChecklistItems(wekanChecklistItems) { + check(wekanChecklistItems, [Match.ObjectIncluding({ + cardId: String, + title: String, })]); } // You must call parseActions before calling this one. - createBoardAndLabels(wekanBoard) { + createBoardAndLabels(boardToImport) { const boardToCreate = { - archived: wekanBoard.archived, - color: wekanBoard.color, + archived: boardToImport.archived, + color: boardToImport.color, // very old boards won't have a creation activity so no creation date - createdAt: this._now(wekanBoard.createdAt), + createdAt: this._now(boardToImport.createdAt), labels: [], members: [{ userId: Meteor.userId(), - isAdmin: true, + wekanId: Meteor.userId(), isActive: true, + isAdmin: true, isCommentOnly: false, + swimlaneId: false, }], // Standalone Export has modifiedAt missing, adding modifiedAt to fix it - modifiedAt: this._now(wekanBoard.modifiedAt), - permission: wekanBoard.permission, - slug: getSlug(wekanBoard.title) || 'board', + modifiedAt: this._now(boardToImport.modifiedAt), + permission: boardToImport.permission, + slug: getSlug(boardToImport.title) || 'board', stars: 0, - title: wekanBoard.title, + title: boardToImport.title, }; // now add other members - if(wekanBoard.members) { - wekanBoard.members.forEach((wekanMember) => { - const wekanId = wekanMember.userId; - // do we have a mapping? - if(this.members[wekanId]) { - const wekanId = this.members[wekanId]; - // do we already have it in our list? - const wekanMember = boardToCreate.members.find((wekanMember) => wekanMember.userId === wekanId); - if(!wekanMember) { - boardToCreate.members.push({ - userId: wekanId, - isAdmin: wekanMember.isAdmin, - isActive: true, - isCommentOnly: false, - }); - } - } + if(boardToImport.members) { + boardToImport.members.forEach((wekanMember) => { + // do we already have it in our list? + if(!boardToCreate.members.some((member) => member.wekanId === wekanMember.wekanId)) + boardToCreate.members.push({ + ... wekanMember, + userId: wekanMember.wekanId, + }); }); } - wekanBoard.labels.forEach((label) => { + boardToImport.labels.forEach((label) => { const labelToCreate = { _id: Random.id(6), color: label.color, @@ -192,7 +200,7 @@ export class WekanCreator { boardId, createdAt: this._now(), source: { - id: wekanBoard.id, + id: boardToImport.id, system: 'Wekan', }, // We attribute the import to current user, @@ -220,11 +228,15 @@ export class WekanCreator { dateLastActivity: this._now(), description: card.description, listId: this.lists[card.listId], + swimlaneId: this.swimlanes[card.swimlaneId], sort: card.sort, title: card.title, // we attribute the card to its creator if available userId: this._user(this.createdBy.cards[card._id]), + isOvertime: card.isOvertime || false, + startAt: card.startAt ? this._now(card.startAt) : null, dueAt: card.dueAt ? this._now(card.dueAt) : null, + spentTime: card.spentTime || null, }; // add labels if (card.labelIds) { @@ -378,7 +390,7 @@ export class WekanCreator { } createLists(wekanLists, boardId) { - wekanLists.forEach((list) => { + wekanLists.forEach((list, listIndex) => { const listToCreate = { archived: list.archived, boardId, @@ -388,6 +400,7 @@ export class WekanCreator { // we require. createdAt: this._now(this.createdAt.lists[list.id]), title: list.title, + sort: list.sort ? list.sort : listIndex, }; const listId = Lists.direct.insert(listToCreate); Lists.direct.update(listId, {$set: {'updatedAt': this._now()}}); @@ -409,7 +422,27 @@ export class WekanCreator { }); } + createSwimlanes(wekanSwimlanes, boardId) { + wekanSwimlanes.forEach((swimlane, swimlaneIndex) => { + const swimlaneToCreate = { + archived: swimlane.archived, + boardId, + // We are being defensing here by providing a default date (now) if the + // creation date wasn't found on the action log. This happen on old + // Wekan boards (eg from 2013) that didn't log the 'createList' action + // we require. + createdAt: this._now(this.createdAt.swimlanes[swimlane._id]), + title: swimlane.title, + sort: swimlane.sort ? swimlane.sort : swimlaneIndex, + }; + const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate); + Swimlanes.direct.update(swimlaneId, {$set: {'updatedAt': this._now()}}); + this.swimlanes[swimlane._id] = swimlaneId; + }); + } + createChecklists(wekanChecklists) { + const result = []; wekanChecklists.forEach((checklist, checklistIndex) => { // Create the checklist const checklistToCreate = { @@ -419,19 +452,24 @@ export class WekanCreator { sort: checklist.sort ? checklist.sort : checklistIndex, }; const checklistId = Checklists.direct.insert(checklistToCreate); - // keep track of Wekan id => WeKan id this.checklists[checklist._id] = checklistId; - // Now add the items to the checklist - const itemsToCreate = []; - checklist.items.forEach((item, itemIndex) => { - itemsToCreate.push({ - _id: checklistId + itemsToCreate.length, - title: item.title, - isFinished: item.isFinished, - sort: item.sort ? item.sort : itemIndex, - }); - }); - Checklists.direct.update(checklistId, {$set: {items: itemsToCreate}}); + result.push(checklistId); + }); + return result; + } + + createChecklistItems(wekanChecklistItems) { + wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => { + // Create the checklistItem + const checklistItemTocreate = { + title: checklistitem.title, + checklistId: this.checklists[checklistitem.checklistId], + cardId: this.cards[checklistitem.cardId], + sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex, + isFinished: checklistitem.isFinished, + }; + const checklistItemId = ChecklistItems.direct.insert(checklistItemTocreate); + this.checklistItems[checklistitem._id] = checklistItemId; }); } @@ -445,14 +483,17 @@ export class WekanCreator { const wekanAttachment = wekanBoard.attachments.filter((attachment) => { return attachment._id === activity.attachmentId; })[0]; - if(wekanAttachment.url || wekanAttachment.file) { + + if ( typeof wekanAttachment !== 'undefined' && wekanAttachment ) { + if(wekanAttachment.url || wekanAttachment.file) { // we cannot actually create the Wekan attachment, because we don't yet // have the cards to attach it to, so we store it in the instance variable. - const wekanCardId = activity.cardId; - if(!this.attachments[wekanCardId]) { - this.attachments[wekanCardId] = []; + const wekanCardId = activity.cardId; + if(!this.attachments[wekanCardId]) { + this.attachments[wekanCardId] = []; + } + this.attachments[wekanCardId].push(wekanAttachment); } - this.attachments[wekanCardId].push(wekanAttachment); } break; } @@ -481,6 +522,11 @@ export class WekanCreator { const listId = activity.listId; this.createdAt.lists[listId] = activity.createdAt; break; + } + case 'createSwimlane': { + const swimlaneId = activity.swimlaneId; + this.createdAt.swimlanes[swimlaneId] = activity.createdAt; + break; }} }); } @@ -602,8 +648,10 @@ export class WekanCreator { this.checkBoard(board); this.checkLabels(board.labels); this.checkLists(board.lists); + this.checkSwimlanes(board.swimlanes); this.checkCards(board.cards); this.checkChecklists(board.checklists); + this.checkChecklistItems(board.checklistItems); } catch (e) { throw new Meteor.Error('error-json-schema'); } @@ -620,8 +668,10 @@ export class WekanCreator { this.parseActivities(board); const boardId = this.createBoardAndLabels(board); this.createLists(board.lists, boardId); + this.createSwimlanes(board.swimlanes, boardId); this.createCards(board.cards, boardId); this.createChecklists(board.checklists); + this.createChecklistItems(board.checklistItems); this.importActivities(board.activities, boardId); // XXX add members return boardId; |