From c60e80d25baa6a81b28f6090ca848553d20b2bb7 Mon Sep 17 00:00:00 2001 From: Justin Reynolds Date: Wed, 26 Jun 2019 17:47:27 -0500 Subject: Add createdAt and modifiedAt to all collections --- models/accountSettings.js | 90 +++-- models/actions.js | 15 + models/activities.js | 45 ++- models/announcements.js | 74 +++- models/attachments.js | 19 +- models/avatars.js | 10 +- models/boards.js | 953 +++++++++++++++++++++++++--------------------- models/cardComments.js | 280 ++++++++------ models/cards.js | 25 +- models/checklistItems.js | 229 ++++++----- models/checklists.js | 313 ++++++++------- models/customFields.js | 297 +++++++++------ models/integrations.js | 336 +++++++++------- models/invitationCodes.js | 87 +++-- models/lists.js | 361 ++++++++++-------- models/rules.js | 86 +++-- models/settings.js | 262 ++++++++----- models/swimlanes.js | 336 +++++++++------- models/triggers.js | 16 +- models/unsavedEdits.js | 73 +++- models/users.js | 889 +++++++++++++++++++++++------------------- 21 files changed, 2857 insertions(+), 1939 deletions(-) (limited to 'models') diff --git a/models/accountSettings.js b/models/accountSettings.js index 6dfbac5d..c4240f84 100644 --- a/models/accountSettings.js +++ b/models/accountSettings.js @@ -1,18 +1,44 @@ AccountSettings = new Mongo.Collection('accountSettings'); -AccountSettings.attachSchema(new SimpleSchema({ - _id: { - type: String, - }, - booleanValue: { - type: Boolean, - optional: true, - }, - sort: { - type: Number, - decimal: true, - }, -})); +AccountSettings.attachSchema( + new SimpleSchema({ + _id: { + type: String, + }, + booleanValue: { + type: Boolean, + optional: true, + }, + sort: { + type: Number, + decimal: true, + }, + createdAt: { + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }) +); AccountSettings.allow({ update(userId) { @@ -21,19 +47,33 @@ AccountSettings.allow({ }, }); +AccountSettings.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + if (Meteor.isServer) { Meteor.startup(() => { - AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, { - $setOnInsert: { - booleanValue: false, - sort: 0, - }, - }); - AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, { - $setOnInsert: { - booleanValue: false, - sort: 1, - }, - }); + AccountSettings._collection._ensureIndex({ modifiedAt: -1 }); + AccountSettings.upsert( + { _id: 'accounts-allowEmailChange' }, + { + $setOnInsert: { + booleanValue: false, + sort: 0, + }, + } + ); + AccountSettings.upsert( + { _id: 'accounts-allowUserNameChange' }, + { + $setOnInsert: { + booleanValue: false, + sort: 1, + }, + } + ); }); } + +export default AccountSettings; diff --git a/models/actions.js b/models/actions.js index 0430b044..8ac764aa 100644 --- a/models/actions.js +++ b/models/actions.js @@ -1,3 +1,5 @@ +import { Meteor } from 'meteor/meteor'; + Actions = new Mongo.Collection('actions'); Actions.allow({ @@ -17,3 +19,16 @@ Actions.helpers({ return this.desc; }, }); + +Actions.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + +if (Meteor.isServer) { + Meteor.startup(() => { + Actions._collection._ensureIndex({ modifiedAt: -1 }); + }); +} + +export default Actions; diff --git a/models/activities.js b/models/activities.js index 908d4b14..0e158802 100644 --- a/models/activities.js +++ b/models/activities.js @@ -69,7 +69,11 @@ Activities.before.insert((userId, doc) => { Activities.after.insert((userId, doc) => { const activity = Activities._transform(doc); RulesHelper.executeRules(activity); +}); +Activities.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); }); if (Meteor.isServer) { @@ -78,11 +82,21 @@ if (Meteor.isServer) { // are largely used in the App. See #524. Meteor.startup(() => { Activities._collection._ensureIndex({ createdAt: -1 }); + Activities._collection._ensureIndex({ modifiedAt: -1 }); Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 }); Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 }); - Activities._collection._ensureIndex({ commentId: 1 }, { partialFilterExpression: { commentId: { $exists: true } } }); - Activities._collection._ensureIndex({ attachmentId: 1 }, { partialFilterExpression: { attachmentId: { $exists: true } } }); - Activities._collection._ensureIndex({ customFieldId: 1 }, { partialFilterExpression: { customFieldId: { $exists: true } } }); + Activities._collection._ensureIndex( + { commentId: 1 }, + { partialFilterExpression: { commentId: { $exists: true } } } + ); + Activities._collection._ensureIndex( + { attachmentId: 1 }, + { partialFilterExpression: { attachmentId: { $exists: true } } } + ); + Activities._collection._ensureIndex( + { customFieldId: 1 }, + { partialFilterExpression: { customFieldId: { $exists: true } } } + ); // Label activity did not work yet, unable to edit labels when tried this. //Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 }); //Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } }); @@ -189,18 +203,35 @@ if (Meteor.isServer) { // params.labelId = activity.labelId; //} if (board) { - const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId'); - const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId'); - watchers = _.union(watchers, watchingUsers, _.intersection(participants, trackingUsers)); + const watchingUsers = _.pluck( + _.where(board.watchers, { level: 'watching' }), + 'userId' + ); + const trackingUsers = _.pluck( + _.where(board.watchers, { level: 'tracking' }), + 'userId' + ); + watchers = _.union( + watchers, + watchingUsers, + _.intersection(participants, trackingUsers) + ); } Notifications.getUsers(watchers).forEach((user) => { Notifications.notify(user, title, description, params); }); - const integrations = Integrations.find({ boardId: board._id, type: 'outgoing-webhooks', enabled: true, activities: { '$in': [description, 'all'] } }).fetch(); + const integrations = Integrations.find({ + boardId: board._id, + type: 'outgoing-webhooks', + enabled: true, + activities: { $in: [description, 'all'] }, + }).fetch(); if (integrations.length > 0) { Meteor.call('outgoingWebhooks', integrations, description, params); } }); } + +export default Activities; diff --git a/models/announcements.js b/models/announcements.js index 2cb1e1b7..f3a62244 100644 --- a/models/announcements.js +++ b/models/announcements.js @@ -1,23 +1,49 @@ 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.attachSchema( + new SimpleSchema({ + enabled: { + type: Boolean, + defaultValue: false, + }, + title: { + type: String, + optional: true, + }, + body: { + type: String, + optional: true, + }, + sort: { + type: Number, + decimal: true, + }, + createdAt: { + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }) +); Announcements.allow({ update(userId) { @@ -26,11 +52,19 @@ Announcements.allow({ }, }); +Announcements.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + if (Meteor.isServer) { Meteor.startup(() => { + Announcements._collection._ensureIndex({ modifiedAt: -1 }); const announcements = Announcements.findOne({}); - if(!announcements){ - Announcements.insert({enabled: false, sort: 0}); + if (!announcements) { + Announcements.insert({ enabled: false, sort: 0 }); } }); } + +export default Announcements; diff --git a/models/attachments.js b/models/attachments.js index 71b30eee..893b5aca 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,6 +1,5 @@ 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', { @@ -25,7 +24,6 @@ Attachments = new FS.Collection('attachments', { ], }); - if (Meteor.isServer) { Meteor.startup(() => { Attachments.files._ensureIndex({ cardId: 1 }); @@ -78,13 +76,16 @@ if (Meteor.isServer) { } 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.update( + { + _id: doc._id, }, - }); + { + $unset: { + source: '', + }, + } + ); } }); @@ -107,3 +108,5 @@ if (Meteor.isServer) { }); }); } + +export default Attachments; diff --git a/models/avatars.js b/models/avatars.js index 53924ffb..2fda031d 100644 --- a/models/avatars.js +++ b/models/avatars.js @@ -1,7 +1,5 @@ Avatars = new FS.Collection('avatars', { - stores: [ - new FS.Store.GridFS('avatars'), - ], + stores: [new FS.Store.GridFS('avatars')], filter: { maxSize: 72000, allow: { @@ -18,10 +16,14 @@ Avatars.allow({ insert: isOwner, update: isOwner, remove: isOwner, - download() { return true; }, + download() { + return true; + }, fetch: ['userId'], }); Avatars.files.before.insert((userId, doc) => { doc.userId = userId; }); + +export default Avatars; diff --git a/models/boards.js b/models/boards.js index 396d90fb..2792f80a 100644 --- a/models/boards.js +++ b/models/boards.js @@ -3,316 +3,347 @@ Boards = new Mongo.Collection('boards'); /** * This is a Board. */ -Boards.attachSchema(new SimpleSchema({ - title: { - /** - * The title of the board - */ - type: String, - }, - slug: { - /** - * The title slugified. - */ - type: String, - autoValue() { // eslint-disable-line consistent-return - // XXX We need to improve slug management. Only the id should be necessary - // to identify a board in the code. - // XXX If the board title is updated, the slug should also be updated. - // In some cases (Chinese and Japanese for instance) the `getSlug` function - // return an empty string. This is causes bugs in our application so we set - // a default slug in this case. - if (this.isInsert && !this.isSet) { - let slug = 'board'; - const title = this.field('title'); - if (title.isSet) { - slug = getSlug(title.value) || slug; +Boards.attachSchema( + new SimpleSchema({ + title: { + /** + * The title of the board + */ + type: String, + }, + slug: { + /** + * The title slugified. + */ + type: String, + // eslint-disable-next-line consistent-return + autoValue() { + // XXX We need to improve slug management. Only the id should be necessary + // to identify a board in the code. + // XXX If the board title is updated, the slug should also be updated. + // In some cases (Chinese and Japanese for instance) the `getSlug` function + // return an empty string. This is causes bugs in our application so we set + // a default slug in this case. + if (this.isInsert && !this.isSet) { + let slug = 'board'; + const title = this.field('title'); + if (title.isSet) { + slug = getSlug(title.value) || slug; + } + return slug; } - return slug; - } + }, }, - }, - archived: { - /** - * Is the board archived? - */ - type: Boolean, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return false; - } + archived: { + /** + * Is the board archived? + */ + type: Boolean, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return false; + } + }, }, - }, - createdAt: { - /** - * Creation time of the board - */ - type: Date, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return new Date(); - } else { - this.unset(); - } + createdAt: { + /** + * Creation time of the board + */ + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, }, - }, - // XXX Inconsistent field naming - modifiedAt: { - /** - * Last modification time of the board - */ - type: Date, - optional: true, - autoValue() { // eslint-disable-line consistent-return - if (this.isUpdate) { - return new Date(); - } else { - this.unset(); - } + // XXX Inconsistent field naming + modifiedAt: { + /** + * Last modification time of the board + */ + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, }, - }, - // De-normalized number of users that have starred this board - stars: { - /** - * How many stars the board has - */ - type: Number, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return 0; - } + // De-normalized number of users that have starred this board + stars: { + /** + * How many stars the board has + */ + type: Number, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return 0; + } + }, }, - }, - // De-normalized label system - 'labels': { - /** - * List of labels attached to a board - */ - type: [Object], - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; - const defaultLabelsColors = _.clone(colors).splice(0, 6); - return defaultLabelsColors.map((color) => ({ - color, - _id: Random.id(6), - name: '', - })); - } + // De-normalized label system + labels: { + /** + * List of labels attached to a board + */ + type: [Object], + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + const colors = Boards.simpleSchema()._schema['labels.$.color'] + .allowedValues; + const defaultLabelsColors = _.clone(colors).splice(0, 6); + return defaultLabelsColors.map((color) => ({ + color, + _id: Random.id(6), + name: '', + })); + } + }, }, - }, - 'labels.$._id': { - /** - * Unique id of a label - */ - // We don't specify that this field must be unique in the board because that - // will cause performance penalties and is not necessary since this field is - // always set on the server. - // XXX Actually if we create a new label, the `_id` is set on the client - // without being overwritten by the server, could it be a problem? - type: String, - }, - 'labels.$.name': { - /** - * Name of a label - */ - type: String, - optional: true, - }, - 'labels.$.color': { - /** - * color of a label. - * - * Can be amongst `green`, `yellow`, `orange`, `red`, `purple`, - * `blue`, `sky`, `lime`, `pink`, `black`, - * `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, - * `slateblue`, `magenta`, `gold`, `navy`, `gray`, - * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo` - */ - type: String, - allowedValues: [ - 'green', 'yellow', 'orange', 'red', 'purple', - 'blue', 'sky', 'lime', 'pink', 'black', - 'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen', - 'slateblue', 'magenta', 'gold', 'navy', 'gray', - 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', - ], - }, - // XXX We might want to maintain more informations under the member sub- - // documents like de-normalized meta-data (the date the member joined the - // board, the number of contributions, etc.). - 'members': { - /** - * List of members of a board - */ - type: [Object], - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return [{ - userId: this.userId, - isAdmin: true, - isActive: true, - isNoComments: false, - isCommentOnly: false, - }]; - } + 'labels.$._id': { + /** + * Unique id of a label + */ + // We don't specify that this field must be unique in the board because that + // will cause performance penalties and is not necessary since this field is + // always set on the server. + // XXX Actually if we create a new label, the `_id` is set on the client + // without being overwritten by the server, could it be a problem? + type: String, }, - }, - 'members.$.userId': { - /** - * The uniq ID of the member - */ - type: String, - }, - 'members.$.isAdmin': { - /** - * Is the member an admin of the board? - */ - type: Boolean, - }, - 'members.$.isActive': { - /** - * Is the member active? - */ - type: Boolean, - }, - 'members.$.isNoComments': { - /** - * Is the member not allowed to make comments - */ - type: Boolean, - optional: true, - }, - 'members.$.isCommentOnly': { - /** - * Is the member only allowed to comment on the board - */ - type: Boolean, - optional: true, - }, - permission: { - /** - * visibility of the board - */ - type: String, - allowedValues: ['public', 'private'], - }, - color: { - /** - * The color of the board. - */ - type: String, - allowedValues: [ - 'belize', - 'nephritis', - 'pomegranate', - 'pumpkin', - 'wisteria', - 'midnight', - ], - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return Boards.simpleSchema()._schema.color.allowedValues[0]; - } + 'labels.$.name': { + /** + * Name of a label + */ + type: String, + optional: true, }, - }, - description: { - /** - * The description of the board - */ - type: String, - optional: true, - }, - subtasksDefaultBoardId: { - /** - * The default board ID assigned to subtasks. - */ - type: String, - optional: true, - defaultValue: null, - }, - subtasksDefaultListId: { - /** - * The default List ID assigned to subtasks. - */ - type: String, - optional: true, - defaultValue: null, - }, - allowsSubtasks: { - /** - * Does the board allows subtasks? - */ - type: Boolean, - defaultValue: true, - }, - presentParentTask: { - /** - * Controls how to present the parent task: - * - * - `prefix-with-full-path`: add a prefix with the full path - * - `prefix-with-parent`: add a prefisx with the parent name - * - `subtext-with-full-path`: add a subtext with the full path - * - `subtext-with-parent`: add a subtext with the parent name - * - `no-parent`: does not show the parent at all - */ - type: String, - allowedValues: [ - 'prefix-with-full-path', - 'prefix-with-parent', - 'subtext-with-full-path', - 'subtext-with-parent', - 'no-parent', - ], - optional: true, - defaultValue: 'no-parent', - }, - startAt: { - /** - * Starting date of the board. - */ - type: Date, - optional: true, - }, - dueAt: { - /** - * Due date of the board. - */ - type: Date, - optional: true, - }, - endAt: { - /** - * End date of the board. - */ - type: Date, - optional: true, - }, - spentTime: { - /** - * Time spent in the board. - */ - type: Number, - decimal: true, - optional: true, - }, - isOvertime: { - /** - * Is the board overtimed? - */ - type: Boolean, - defaultValue: false, - optional: true, - }, - type: { - /** - * The type of board - */ - type: String, - defaultValue: 'board', - }, -})); - + 'labels.$.color': { + /** + * color of a label. + * + * Can be amongst `green`, `yellow`, `orange`, `red`, `purple`, + * `blue`, `sky`, `lime`, `pink`, `black`, + * `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, + * `slateblue`, `magenta`, `gold`, `navy`, `gray`, + * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo` + */ + type: String, + allowedValues: [ + 'green', + 'yellow', + 'orange', + 'red', + 'purple', + 'blue', + 'sky', + 'lime', + 'pink', + 'black', + 'silver', + 'peachpuff', + 'crimson', + 'plum', + 'darkgreen', + 'slateblue', + 'magenta', + 'gold', + 'navy', + 'gray', + 'saddlebrown', + 'paleturquoise', + 'mistyrose', + 'indigo', + ], + }, + // XXX We might want to maintain more informations under the member sub- + // documents like de-normalized meta-data (the date the member joined the + // board, the number of contributions, etc.). + members: { + /** + * List of members of a board + */ + type: [Object], + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return [ + { + userId: this.userId, + isAdmin: true, + isActive: true, + isNoComments: false, + isCommentOnly: false, + }, + ]; + } + }, + }, + 'members.$.userId': { + /** + * The uniq ID of the member + */ + type: String, + }, + 'members.$.isAdmin': { + /** + * Is the member an admin of the board? + */ + type: Boolean, + }, + 'members.$.isActive': { + /** + * Is the member active? + */ + type: Boolean, + }, + 'members.$.isNoComments': { + /** + * Is the member not allowed to make comments + */ + type: Boolean, + optional: true, + }, + 'members.$.isCommentOnly': { + /** + * Is the member only allowed to comment on the board + */ + type: Boolean, + optional: true, + }, + permission: { + /** + * visibility of the board + */ + type: String, + allowedValues: ['public', 'private'], + }, + color: { + /** + * The color of the board. + */ + type: String, + allowedValues: [ + 'belize', + 'nephritis', + 'pomegranate', + 'pumpkin', + 'wisteria', + 'midnight', + ], + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return Boards.simpleSchema()._schema.color.allowedValues[0]; + } + }, + }, + description: { + /** + * The description of the board + */ + type: String, + optional: true, + }, + subtasksDefaultBoardId: { + /** + * The default board ID assigned to subtasks. + */ + type: String, + optional: true, + defaultValue: null, + }, + subtasksDefaultListId: { + /** + * The default List ID assigned to subtasks. + */ + type: String, + optional: true, + defaultValue: null, + }, + allowsSubtasks: { + /** + * Does the board allows subtasks? + */ + type: Boolean, + defaultValue: true, + }, + presentParentTask: { + /** + * Controls how to present the parent task: + * + * - `prefix-with-full-path`: add a prefix with the full path + * - `prefix-with-parent`: add a prefisx with the parent name + * - `subtext-with-full-path`: add a subtext with the full path + * - `subtext-with-parent`: add a subtext with the parent name + * - `no-parent`: does not show the parent at all + */ + type: String, + allowedValues: [ + 'prefix-with-full-path', + 'prefix-with-parent', + 'subtext-with-full-path', + 'subtext-with-parent', + 'no-parent', + ], + optional: true, + defaultValue: 'no-parent', + }, + startAt: { + /** + * Starting date of the board. + */ + type: Date, + optional: true, + }, + dueAt: { + /** + * Due date of the board. + */ + type: Date, + optional: true, + }, + endAt: { + /** + * End date of the board. + */ + type: Date, + optional: true, + }, + spentTime: { + /** + * Time spent in the board. + */ + type: Number, + decimal: true, + optional: true, + }, + isOvertime: { + /** + * Is the board overtimed? + */ + type: Boolean, + defaultValue: false, + optional: true, + }, + type: { + /** + * The type of board + */ + type: String, + defaultValue: 'board', + }, + }) +); Boards.helpers({ copy() { @@ -350,7 +381,9 @@ Boards.helpers({ */ isActiveMember(userId) { if (userId) { - return this.members.find((member) => (member.userId === userId && member.isActive)); + return this.members.find( + (member) => member.userId === userId && member.isActive + ); } else { return false; } @@ -361,11 +394,17 @@ Boards.helpers({ }, cards() { - return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } }); + return Cards.find( + { boardId: this._id, archived: false }, + { sort: { title: 1 } } + ); }, lists() { - return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); + return Lists.find( + { boardId: this._id, archived: false }, + { sort: { sort: 1 } } + ); }, nullSortLists() { @@ -377,18 +416,24 @@ Boards.helpers({ }, swimlanes() { - return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); + return Swimlanes.find( + { boardId: this._id, archived: false }, + { sort: { sort: 1 } } + ); }, nextSwimlane(swimlane) { - return Swimlanes.findOne({ - boardId: this._id, - archived: false, - sort: { $gte: swimlane.sort }, - _id: { $ne: swimlane._id }, - }, { - sort: { sort: 1 }, - }); + return Swimlanes.findOne( + { + boardId: this._id, + archived: false, + sort: { $gte: swimlane.sort }, + _id: { $ne: swimlane._id }, + }, + { + sort: { sort: 1 }, + } + ); }, nullSortSwimlanes() { @@ -399,13 +444,21 @@ Boards.helpers({ }); }, - hasOvertimeCards(){ - const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} ); + 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} ); + hasSpentTimeCards() { + const card = Cards.findOne({ + spentTime: { $gt: 0 }, + boardId: this._id, + archived: false, + }); return card !== undefined; }, @@ -429,7 +482,7 @@ Boards.helpers({ return _.findWhere(this.labels, { name, color }); }, - getLabelById(labelId){ + getLabelById(labelId) { return _.findWhere(this.labels, { _id: labelId }); }, @@ -446,15 +499,29 @@ Boards.helpers({ }, hasAdmin(memberId) { - return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true }); + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: true, + }); }, hasNoComments(memberId) { - return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isNoComments: true }); + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: false, + isNoComments: true, + }); }, hasCommentOnly(memberId) { - return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isCommentOnly: true }); + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: false, + isCommentOnly: true, + }); }, absoluteUrl() { @@ -466,7 +533,10 @@ Boards.helpers({ }, customFields() { - return CustomFields.find({ boardIds: {$in: [this._id]} }, { sort: { name: 1 } }); + return CustomFields.find( + { boardIds: { $in: [this._id] } }, + { sort: { name: 1 } } + ); }, // XXX currently mutations return no value so we have an issue when using addLabel in import @@ -489,10 +559,7 @@ Boards.helpers({ if (term) { const regex = new RegExp(term, 'i'); - query.$or = [ - { title: regex }, - { description: regex }, - ]; + query.$or = [{ title: regex }, { description: regex }]; } return Cards.find(query, projection); @@ -506,17 +573,14 @@ Boards.helpers({ query.type = 'template-swimlane'; query.archived = false; } else { - query.type = {$nin: ['template-swimlane']}; + query.type = { $nin: ['template-swimlane'] }; } const projection = { limit: 10, sort: { createdAt: -1 } }; if (term) { const regex = new RegExp(term, 'i'); - query.$or = [ - { title: regex }, - { description: regex }, - ]; + query.$or = [{ title: regex }, { description: regex }]; } return Swimlanes.find(query, projection); @@ -530,17 +594,14 @@ Boards.helpers({ query.type = 'template-list'; query.archived = false; } else { - query.type = {$nin: ['template-list']}; + query.type = { $nin: ['template-list'] }; } const projection = { limit: 10, sort: { createdAt: -1 } }; if (term) { const regex = new RegExp(term, 'i'); - query.$or = [ - { title: regex }, - { description: regex }, - ]; + query.$or = [{ title: regex }, { description: regex }]; } return Lists.find(query, projection); @@ -557,17 +618,14 @@ Boards.helpers({ query.type = 'template-card'; query.archived = false; } else { - query.type = {$nin: ['template-card']}; + query.type = { $nin: ['template-card'] }; } const projection = { limit: 10, sort: { createdAt: -1 } }; if (term) { const regex = new RegExp(term, 'i'); - query.$or = [ - { title: regex }, - { description: regex }, - ]; + query.$or = [{ title: regex }, { description: regex }]; } return Cards.find(query, projection); @@ -575,22 +633,29 @@ Boards.helpers({ // A board alwasy has another board where it deposits subtasks of thasks // that belong to itself. getDefaultSubtasksBoardId() { - if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) { + if ( + this.subtasksDefaultBoardId === null || + this.subtasksDefaultBoardId === undefined + ) { this.subtasksDefaultBoardId = Boards.insert({ title: `^${this.title}^`, permission: this.permission, members: this.members, color: this.color, - description: TAPi18n.__('default-subtasks-board', {board: this.title}), + description: TAPi18n.__('default-subtasks-board', { + board: this.title, + }), }); Swimlanes.insert({ title: TAPi18n.__('default'), boardId: this.subtasksDefaultBoardId, }); - Boards.update(this._id, {$set: { - subtasksDefaultBoardId: this.subtasksDefaultBoardId, - }}); + Boards.update(this._id, { + $set: { + subtasksDefaultBoardId: this.subtasksDefaultBoardId, + }, + }); } return this.subtasksDefaultBoardId; }, @@ -600,7 +665,10 @@ Boards.helpers({ }, getDefaultSubtasksListId() { - if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) { + if ( + this.subtasksDefaultListId === null || + this.subtasksDefaultListId === undefined + ) { this.subtasksDefaultListId = Lists.insert({ title: TAPi18n.__('queue'), boardId: this._id, @@ -615,13 +683,13 @@ Boards.helpers({ }, getDefaultSwimline() { - let result = Swimlanes.findOne({boardId: this._id}); + let result = Swimlanes.findOne({ boardId: this._id }); if (result === undefined) { Swimlanes.insert({ title: TAPi18n.__('default'), boardId: this._id, }); - result = Swimlanes.findOne({boardId: this._id}); + result = Swimlanes.findOne({ boardId: this._id }); } return result; }, @@ -633,19 +701,24 @@ Boards.helpers({ { startAt: { $lte: start, - }, endAt: { + }, + endAt: { $gte: start, }, - }, { + }, + { startAt: { $lte: end, - }, endAt: { + }, + endAt: { $gte: end, }, - }, { + }, + { startAt: { $gte: start, - }, endAt: { + }, + endAt: { $lte: end, }, }, @@ -662,7 +735,6 @@ Boards.helpers({ }, }); - Boards.mutations({ archive() { return { $set: { archived: true } }; @@ -753,7 +825,8 @@ Boards.mutations({ const memberIndex = this.memberIndex(memberId); // we do not allow the only one admin to be removed - const allowRemove = (!this.members[memberIndex].isAdmin) || (this.activeAdmins().length > 1); + const allowRemove = + !this.members[memberIndex].isAdmin || this.activeAdmins().length > 1; if (!allowRemove) { return { $set: { @@ -770,7 +843,13 @@ Boards.mutations({ }; }, - setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly, currentUserId = Meteor.userId()) { + setMemberPermission( + memberId, + isAdmin, + isNoComments, + isCommentOnly, + currentUserId = Meteor.userId() + ) { const memberIndex = this.memberIndex(memberId); // do not allow change permission of self if (memberId === currentUserId) { @@ -804,12 +883,13 @@ Boards.mutations({ }); function boardRemover(userId, doc) { - [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach((element) => { - element.remove({ boardId: doc._id }); - }); + [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach( + (element) => { + element.remove({ boardId: doc._id }); + } + ); } - if (Meteor.isServer) { Boards.allow({ insert: Meteor.userId, @@ -830,25 +910,25 @@ if (Meteor.isServer) { // We can't remove a member if it is the last administrator Boards.deny({ update(userId, doc, fieldNames, modifier) { - if (!_.contains(fieldNames, 'members')) - return false; + if (!_.contains(fieldNames, 'members')) return false; // We only care in case of a $pull operation, ie remove a member - if (!_.isObject(modifier.$pull && modifier.$pull.members)) - return false; + if (!_.isObject(modifier.$pull && modifier.$pull.members)) return false; // If there is more than one admin, it's ok to remove anyone - const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true }).length; - if (nbAdmins > 1) - return false; + const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true }) + .length; + if (nbAdmins > 1) return false; // If all the previous conditions were verified, we can't remove // a user if it's an admin const removedMemberId = modifier.$pull.members.userId; - return Boolean(_.findWhere(doc.members, { - userId: removedMemberId, - isAdmin: true, - })); + return Boolean( + _.findWhere(doc.members, { + userId: removedMemberId, + isAdmin: true, + }) + ); }, fetch: ['members'], }); @@ -882,16 +962,19 @@ if (Meteor.isServer) { } else throw new Meteor.Error('error-board-doesNotExist'); }, }); - } if (Meteor.isServer) { // Let MongoDB ensure that a member is not included twice in the same board Meteor.startup(() => { - Boards._collection._ensureIndex({ - _id: 1, - 'members.userId': 1, - }, { unique: true }); + Boards._collection._ensureIndex({ modifiedAt: -1 }); + Boards._collection._ensureIndex( + { + _id: 1, + 'members.userId': 1, + }, + { unique: true } + ); Boards._collection._ensureIndex({ 'members.userId': 1 }); }); @@ -909,10 +992,12 @@ if (Meteor.isServer) { // If the user remove one label from a board, we cant to remove reference of // this label in any card of this board. Boards.after.update((userId, doc, fieldNames, modifier) => { - if (!_.contains(fieldNames, 'labels') || + if ( + !_.contains(fieldNames, 'labels') || !modifier.$pull || !modifier.$pull.labels || - !modifier.$pull.labels._id) { + !modifier.$pull.labels._id + ) { return; } @@ -935,12 +1020,21 @@ if (Meteor.isServer) { } const parts = set.split('.'); - if (parts.length === 3 && parts[0] === 'members' && parts[2] === 'isActive') { + if ( + parts.length === 3 && + parts[0] === 'members' && + parts[2] === 'isActive' + ) { callback(doc.members[parts[1]].userId); } }); }; + Boards.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); + }); + // Remove a member from all objects of the board before leaving the board Boards.before.update((userId, doc, fieldNames, modifier) => { if (!_.contains(fieldNames, 'members')) { @@ -976,14 +1070,11 @@ if (Meteor.isServer) { // Remove board from users starred list if (!board.isPublic()) { - Users.update( - memberId, - { - $pull: { - 'profile.starredBoards': boardId, - }, - } - ); + Users.update(memberId, { + $pull: { + 'profile.starredBoards': boardId, + }, + }); } }); } @@ -1044,29 +1135,34 @@ if (Meteor.isServer) { * @return_type [{_id: string, title: string}] */ - JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) { + 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); + Authentication.checkAdminOrCondition( + req.userId, + req.userId === paramUserId + ); - const data = Boards.find({ - archived: false, - 'members.userId': paramUserId, - }, { - sort: ['title'], - }).map(function(board) { + 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}); - } - catch (error) { + JsonRoutes.sendResult(res, { code: 200, data }); + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1081,20 +1177,19 @@ if (Meteor.isServer) { * @return_type [{_id: string, title: string}] */ - JsonRoutes.add('GET', '/api/boards', function (req, res) { + 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) { + data: Boards.find({ permission: 'public' }).map(function(doc) { return { _id: doc._id, title: doc.title, }; }), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1109,7 +1204,7 @@ if (Meteor.isServer) { * @param {string} boardId the ID of the board to retrieve the data * @return_type Boards */ - JsonRoutes.add('GET', '/api/boards/:boardId', function (req, res) { + JsonRoutes.add('GET', '/api/boards/:boardId', function(req, res) { try { const id = req.params.boardId; Authentication.checkBoardAccess(req.userId, id); @@ -1118,8 +1213,7 @@ if (Meteor.isServer) { code: 200, data: Boards.findOne({ _id: id }), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1152,7 +1246,7 @@ if (Meteor.isServer) { * @return_type {_id: string, defaultSwimlaneId: string} */ - JsonRoutes.add('POST', '/api/boards', function (req, res) { + JsonRoutes.add('POST', '/api/boards', function(req, res) { try { Authentication.checkUserId(req.userId); const id = Boards.insert({ @@ -1180,8 +1274,7 @@ if (Meteor.isServer) { defaultSwimlaneId: swimlaneId, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1195,19 +1288,18 @@ if (Meteor.isServer) { * * @param {string} boardId the ID of the board */ - JsonRoutes.add('DELETE', '/api/boards/:boardId', function (req, res) { + JsonRoutes.add('DELETE', '/api/boards/:boardId', function(req, res) { try { Authentication.checkUserId(req.userId); const id = req.params.boardId; Boards.remove({ _id: id }); JsonRoutes.sendResult(res, { code: 200, - data:{ + data: { _id: id, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1228,7 +1320,7 @@ if (Meteor.isServer) { * * @return_type string */ - JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function (req, res) { + JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function(req, res) { Authentication.checkUserId(req.userId); const id = req.params.boardId; try { @@ -1238,7 +1330,10 @@ if (Meteor.isServer) { 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 } } }); + Boards.direct.update( + { _id: id }, + { $push: { labels: { _id: labelId, name, color } } } + ); JsonRoutes.sendResult(res, { code: 200, data: labelId, @@ -1249,8 +1344,7 @@ if (Meteor.isServer) { }); } } - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { data: error, }); @@ -1268,29 +1362,36 @@ if (Meteor.isServer) { * @param {boolean} isNoComments NoComments capability * @param {boolean} isCommentOnly CommentsOnly capability */ - JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function (req, res) { + JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function( + req, + res + ) { try { const boardId = req.params.boardId; const memberId = req.params.memberId; - const {isAdmin, isNoComments, isCommentOnly} = req.body; + const { isAdmin, isNoComments, isCommentOnly } = req.body; Authentication.checkBoardAccess(req.userId, boardId); const board = Boards.findOne({ _id: boardId }); - function isTrue(data){ + function isTrue(data) { try { return data.toLowerCase() === 'true'; - } - catch (error) { + } catch (error) { return data; } } - const query = board.setMemberPermission(memberId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), req.userId); + const query = board.setMemberPermission( + memberId, + isTrue(isAdmin), + isTrue(isNoComments), + isTrue(isCommentOnly), + req.userId + ); JsonRoutes.sendResult(res, { code: 200, data: query, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1298,3 +1399,5 @@ if (Meteor.isServer) { } }); } + +export default Boards; diff --git a/models/cardComments.js b/models/cardComments.js index a823066c..8f727aa0 100644 --- a/models/cardComments.js +++ b/models/cardComments.js @@ -3,55 +3,69 @@ CardComments = new Mongo.Collection('card_comments'); /** * A comment on a card */ -CardComments.attachSchema(new SimpleSchema({ - boardId: { - /** - * the board ID - */ - type: String, - }, - cardId: { - /** - * the card ID - */ - type: String, - }, - // XXX Rename in `content`? `text` is a bit vague... - text: { - /** - * the text of the comment - */ - type: String, - }, - // XXX We probably don't need this information here, since we already have it - // in the associated comment creation activity - createdAt: { - /** - * when was the comment created - */ - type: Date, - denyUpdate: false, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return new Date(); - } else { - this.unset(); - } +CardComments.attachSchema( + new SimpleSchema({ + boardId: { + /** + * the board ID + */ + type: String, }, - }, - // XXX Should probably be called `authorId` - userId: { - /** - * the author ID of the comment - */ - type: String, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return this.userId; - } + cardId: { + /** + * the card ID + */ + type: String, }, - }, -})); + // XXX Rename in `content`? `text` is a bit vague... + text: { + /** + * the text of the comment + */ + type: String, + }, + createdAt: { + /** + * when was the comment created + */ + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + // XXX Should probably be called `authorId` + userId: { + /** + * the author ID of the comment + */ + type: String, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return this.userId; + } + }, + }, + }) +); CardComments.allow({ insert(userId, doc) { @@ -80,7 +94,7 @@ CardComments.helpers({ CardComments.hookOptions.after.update = { fetchPrevious: false }; -function commentCreation(userId, doc){ +function commentCreation(userId, doc) { const card = Cards.findOne(doc.cardId); Activities.insert({ userId, @@ -93,10 +107,16 @@ function commentCreation(userId, doc){ }); } +CardComments.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + if (Meteor.isServer) { // Comments are often fetched within a card, so we create an index to make these // queries more efficient. Meteor.startup(() => { + CardComments._collection._ensureIndex({ modifiedAt: -1 }); CardComments._collection._ensureIndex({ cardId: 1, createdAt: -1 }); }); @@ -152,14 +172,20 @@ if (Meteor.isServer) { * comment: string, * authorId: string}] */ - JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) { + JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function( + req, + res + ) { try { - Authentication.checkUserId( req.userId); + 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) { + data: CardComments.find({ + boardId: paramBoardId, + cardId: paramCardId, + }).map(function(doc) { return { _id: doc._id, comment: doc.text, @@ -167,8 +193,7 @@ if (Meteor.isServer) { }; }), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -185,24 +210,31 @@ if (Meteor.isServer) { * @param {string} commentId the ID of the comment to retrieve * @return_type CardComments */ - 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( + '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, + }); + } } - }); + ); /** * @operation new_comment @@ -214,35 +246,42 @@ if (Meteor.isServer) { * @param {string} text the content of the comment * @return_type {_id: string} */ - 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, - }); + 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, + }); - 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, - }); + const cardComment = CardComments.findOne({ + _id: id, + cardId: paramCardId, + boardId: paramBoardId, + }); + commentCreation(req.body.authorId, cardComment); + } catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } } - }); + ); /** * @operation delete_comment @@ -253,25 +292,34 @@ if (Meteor.isServer) { * @param {string} commentId the ID of the comment to delete * @return_type {_id: string} */ - 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, - }); + 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, + }); + } } - }); + ); } + +export default CardComments; diff --git a/models/cards.js b/models/cards.js index fdb7deb3..b873c086 100644 --- a/models/cards.js +++ b/models/cards.js @@ -81,7 +81,8 @@ Cards.attachSchema(new SimpleSchema({ * creation date */ type: Date, - autoValue() { // eslint-disable-line consistent-return + // eslint-disable-next-line consistent-return + autoValue() { if (this.isInsert) { return new Date(); } else { @@ -89,6 +90,18 @@ Cards.attachSchema(new SimpleSchema({ } }, }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, customFields: { /** * list of custom fields @@ -1539,7 +1552,8 @@ if (Meteor.isServer) { // 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}); + Cards._collection._ensureIndex({ modifiedAt: -1 }); + Cards._collection._ensureIndex({ boardId: 1, createdAt: -1 }); // https://github.com/wekan/wekan/issues/1863 // Swimlane added a new field in the cards collection of mongodb named parentId. // When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection). @@ -1581,6 +1595,11 @@ if (Meteor.isServer) { cardCustomFields(userId, doc, fieldNames, modifier); }); + Cards.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); + }); + // Remove all activities associated with a card if we remove the card // Remove also card_comments / checklists / attachments Cards.before.remove((userId, doc) => { @@ -1980,3 +1999,5 @@ if (Meteor.isServer) { }); } + +export default Cards; diff --git a/models/checklistItems.js b/models/checklistItems.js index df56c475..d548e681 100644 --- a/models/checklistItems.js +++ b/models/checklistItems.js @@ -3,40 +3,66 @@ ChecklistItems = new Mongo.Collection('checklistItems'); /** * An item in a checklist */ -ChecklistItems.attachSchema(new SimpleSchema({ - title: { - /** - * the text of the item - */ - type: String, - }, - sort: { - /** - * the sorting field of the item - */ - type: Number, - decimal: true, - }, - isFinished: { - /** - * Is the item checked? - */ - type: Boolean, - defaultValue: false, - }, - checklistId: { - /** - * the checklist ID the item is attached to - */ - type: String, - }, - cardId: { - /** - * the card ID the item is attached to - */ - type: String, - }, -})); +ChecklistItems.attachSchema( + new SimpleSchema({ + title: { + /** + * the text of the item + */ + type: String, + }, + sort: { + /** + * the sorting field of the item + */ + type: Number, + decimal: true, + }, + isFinished: { + /** + * Is the item checked? + */ + type: Boolean, + defaultValue: false, + }, + checklistId: { + /** + * the checklist ID the item is attached to + */ + type: String, + }, + cardId: { + /** + * the card ID the item is attached to + */ + type: String, + }, + createdAt: { + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }) +); ChecklistItems.allow({ insert(userId, doc) { @@ -62,10 +88,10 @@ ChecklistItems.mutations({ setTitle(title) { return { $set: { title } }; }, - check(){ + check() { return { $set: { isFinished: true } }; }, - uncheck(){ + uncheck() { return { $set: { isFinished: false } }; }, toggleItem() { @@ -79,7 +105,7 @@ ChecklistItems.mutations({ sort: sortIndex, }; - return {$set: mutatedFields}; + return { $set: mutatedFields }; }, }); @@ -106,13 +132,13 @@ function itemRemover(userId, doc) { }); } -function publishCheckActivity(userId, doc){ +function publishCheckActivity(userId, doc) { const card = Cards.findOne(doc.cardId); const boardId = card.boardId; let activityType; - if(doc.isFinished){ + if (doc.isFinished) { activityType = 'checkedItem'; - }else{ + } else { activityType = 'uncheckedItem'; } const act = { @@ -122,19 +148,19 @@ function publishCheckActivity(userId, doc){ boardId, checklistId: doc.checklistId, checklistItemId: doc._id, - checklistItemName:doc.title, + checklistItemName: doc.title, listId: card.listId, swimlaneId: card.swimlaneId, }; Activities.insert(act); } -function publishChekListCompleted(userId, doc){ +function publishChekListCompleted(userId, doc) { const card = Cards.findOne(doc.cardId); const boardId = card.boardId; const checklistId = doc.checklistId; - const checkList = Checklists.findOne({_id:checklistId}); - if(checkList.isFinished()){ + const checkList = Checklists.findOne({ _id: checklistId }); + if (checkList.isFinished()) { const act = { userId, activityType: 'completeChecklist', @@ -149,11 +175,11 @@ function publishChekListCompleted(userId, doc){ } } -function publishChekListUncompleted(userId, doc){ +function publishChekListUncompleted(userId, doc) { const card = Cards.findOne(doc.cardId); const boardId = card.boardId; const checklistId = doc.checklistId; - const checkList = Checklists.findOne({_id:checklistId}); + const checkList = Checklists.findOne({ _id: checklistId }); // BUGS in IFTTT Rules: https://github.com/wekan/wekan/issues/1972 // Currently in checklist all are set as uncompleted/not checked, // IFTTT Rule does not move card to other list. @@ -167,7 +193,7 @@ function publishChekListUncompleted(userId, doc){ // find . | xargs grep 'count' -sl | grep -v .meteor | grep -v node_modules | grep -v .build // Maybe something related here? // wekan/client/components/rules/triggers/checklistTriggers.js - if(checkList.isFinished()){ + if (checkList.isFinished()) { const act = { userId, activityType: 'uncompleteChecklist', @@ -185,6 +211,7 @@ function publishChekListUncompleted(userId, doc){ // Activities if (Meteor.isServer) { Meteor.startup(() => { + ChecklistItems._collection._ensureIndex({ modifiedAt: -1 }); ChecklistItems._collection._ensureIndex({ checklistId: 1 }); ChecklistItems._collection._ensureIndex({ cardId: 1 }); }); @@ -198,6 +225,10 @@ if (Meteor.isServer) { publishChekListUncompleted(userId, doc, fieldNames); }); + ChecklistItems.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); + }); ChecklistItems.after.insert((userId, doc) => { itemCreation(userId, doc); @@ -214,7 +245,7 @@ if (Meteor.isServer) { boardId, checklistId: doc.checklistId, checklistItemId: doc._id, - checklistItemName:doc.title, + checklistItemName: doc.title, listId: card.listId, swimlaneId: card.swimlaneId, }); @@ -233,21 +264,25 @@ if (Meteor.isServer) { * @param {string} itemId the ID of the item * @return_type ChecklistItems */ - 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( + '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, + }); + } } - }); + ); /** * @operation edit_checklist_item @@ -262,25 +297,35 @@ if (Meteor.isServer) { * @param {string} [title] the new text of the item * @return_type {_id: string} */ - JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { - Authentication.checkUserId( req.userId); + JsonRoutes.add( + 'PUT', + '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', + function(req, res) { + Authentication.checkUserId(req.userId); - const paramItemId = req.params.itemId; + 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}}); - } + 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.sendResult(res, { + code: 200, + data: { + _id: paramItemId, + }, + }); + } + ); /** * @operation delete_checklist_item @@ -295,15 +340,21 @@ if (Meteor.isServer) { * @param {string} itemId the ID of the item to be removed * @return_type {_id: string} */ - 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, - }, - }); - }); + 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, + }, + }); + } + ); } + +export default ChecklistItems; diff --git a/models/checklists.js b/models/checklists.js index 653fed4d..6fd22702 100644 --- a/models/checklists.js +++ b/models/checklists.js @@ -3,49 +3,64 @@ Checklists = new Mongo.Collection('checklists'); /** * A Checklist */ -Checklists.attachSchema(new SimpleSchema({ - cardId: { - /** - * The ID of the card the checklist is in - */ - type: String, - }, - title: { - /** - * the title of the checklist - */ - type: String, - defaultValue: 'Checklist', - }, - finishedAt: { - /** - * When was the checklist finished - */ - type: Date, - optional: true, - }, - createdAt: { - /** - * Creation date of the checklist - */ - type: Date, - denyUpdate: false, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return new Date(); - } else { - this.unset(); - } +Checklists.attachSchema( + new SimpleSchema({ + cardId: { + /** + * The ID of the card the checklist is in + */ + type: String, }, - }, - sort: { - /** - * sorting value of the checklist - */ - type: Number, - decimal: true, - }, -})); + title: { + /** + * the title of the checklist + */ + type: String, + defaultValue: 'Checklist', + }, + finishedAt: { + /** + * When was the checklist finished + */ + type: Date, + optional: true, + }, + createdAt: { + /** + * Creation date of the checklist + */ + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + sort: { + /** + * sorting value of the checklist + */ + type: Number, + decimal: true, + }, + }) +); Checklists.helpers({ copy(newCardId) { @@ -53,7 +68,7 @@ Checklists.helpers({ this._id = null; this.cardId = newCardId; const newChecklistId = Checklists.insert(this); - ChecklistItems.find({checklistId: oldChecklistId}).forEach((item) => { + ChecklistItems.find({ checklistId: oldChecklistId }).forEach((item) => { item._id = null; item.checklistId = newChecklistId; item.cardId = newCardId; @@ -65,9 +80,12 @@ Checklists.helpers({ return ChecklistItems.find({ checklistId: this._id }).count(); }, items() { - return ChecklistItems.find({ - checklistId: this._id, - }, { sort: ['sort'] }); + return ChecklistItems.find( + { + checklistId: this._id, + }, + { sort: ['sort'] } + ); }, finishedCount() { return ChecklistItems.find({ @@ -78,20 +96,20 @@ Checklists.helpers({ isFinished() { return 0 !== this.itemCount() && this.itemCount() === this.finishedCount(); }, - checkAllItems(){ - const checkItems = ChecklistItems.find({checklistId: this._id}); - checkItems.forEach(function(item){ + checkAllItems() { + const checkItems = ChecklistItems.find({ checklistId: this._id }); + checkItems.forEach(function(item) { item.check(); }); }, - uncheckAllItems(){ - const checkItems = ChecklistItems.find({checklistId: this._id}); - checkItems.forEach(function(item){ + uncheckAllItems() { + const checkItems = ChecklistItems.find({ checklistId: this._id }); + checkItems.forEach(function(item) { item.uncheck(); }); }, itemIndex(itemId) { - const items = self.findOne({_id : this._id}).items; + const items = self.findOne({ _id: this._id }).items; return _.pluck(items, '_id').indexOf(itemId); }, }); @@ -124,6 +142,7 @@ Checklists.mutations({ if (Meteor.isServer) { Meteor.startup(() => { + Checklists._collection._ensureIndex({ modifiedAt: -1 }); Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 }); }); @@ -135,12 +154,17 @@ if (Meteor.isServer) { cardId: doc.cardId, boardId: card.boardId, checklistId: doc._id, - checklistName:doc.title, + checklistName: doc.title, listId: card.listId, swimlaneId: card.swimlaneId, }); }); + Checklists.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); + }); + Checklists.before.remove((userId, doc) => { const activities = Activities.find({ checklistId: doc._id }); const card = Cards.findOne(doc.cardId); @@ -155,7 +179,7 @@ if (Meteor.isServer) { cardId: doc.cardId, boardId: Cards.findOne(doc.cardId).boardId, checklistId: doc._id, - checklistName:doc.title, + checklistName: doc.title, listId: card.listId, swimlaneId: card.swimlaneId, }); @@ -172,26 +196,32 @@ if (Meteor.isServer) { * @return_type [{_id: string, * title: string}] */ - JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { - Authentication.checkUserId( req.userId); - const paramCardId = req.params.cardId; - 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', + function(req, res) { + Authentication.checkUserId(req.userId); + const paramCardId = req.params.cardId; + 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, + }); + } } - }); + ); /** * @operation get_checklist @@ -209,29 +239,38 @@ if (Meteor.isServer) { * title: string, * isFinished: boolean}]} */ - 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; - 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( + '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; + 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, + }); + } } - }); + ); /** * @operation new_checklist @@ -242,36 +281,40 @@ if (Meteor.isServer) { * @param {string} title the title of the new checklist * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { - Authentication.checkUserId( req.userId); + JsonRoutes.add( + 'POST', + '/api/boards/:boardId/cards/:cardId/checklists', + function(req, res) { + Authentication.checkUserId(req.userId); - 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, + 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, + }); + } } - }); + ); /** * @operation delete_checklist @@ -284,15 +327,21 @@ if (Meteor.isServer) { * @param {string} checklistId the ID of the checklist to remove * @return_type {_id: string} */ - JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { - Authentication.checkUserId( req.userId); - const paramChecklistId = req.params.checklistId; - Checklists.remove({ _id: paramChecklistId }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: paramChecklistId, - }, - }); - }); + JsonRoutes.add( + 'DELETE', + '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', + function(req, res) { + Authentication.checkUserId(req.userId); + const paramChecklistId = req.params.checklistId; + Checklists.remove({ _id: paramChecklistId }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramChecklistId, + }, + }); + } + ); } + +export default Checklists; diff --git a/models/customFields.js b/models/customFields.js index 83033cb4..8b51c0a3 100644 --- a/models/customFields.js +++ b/models/customFields.js @@ -3,74 +3,100 @@ CustomFields = new Mongo.Collection('customFields'); /** * A custom field on a card in the board */ -CustomFields.attachSchema(new SimpleSchema({ - boardIds: { - /** - * the ID of the board - */ - type: [String], - }, - name: { - /** - * name of the custom field - */ - type: String, - }, - type: { - /** - * type of the custom field - */ - type: String, - allowedValues: ['text', 'number', 'date', 'dropdown'], - }, - settings: { - /** - * settings of the custom field - */ - type: Object, - }, - 'settings.dropdownItems': { - /** - * list of drop down items objects - */ - type: [Object], - optional: true, - }, - 'settings.dropdownItems.$': { - type: new SimpleSchema({ - _id: { - /** - * ID of the drop down item - */ - type: String, +CustomFields.attachSchema( + new SimpleSchema({ + boardIds: { + /** + * the ID of the board + */ + type: [String], + }, + name: { + /** + * name of the custom field + */ + type: String, + }, + type: { + /** + * type of the custom field + */ + type: String, + allowedValues: ['text', 'number', 'date', 'dropdown'], + }, + settings: { + /** + * settings of the custom field + */ + type: Object, + }, + 'settings.dropdownItems': { + /** + * list of drop down items objects + */ + type: [Object], + optional: true, + }, + 'settings.dropdownItems.$': { + type: new SimpleSchema({ + _id: { + /** + * ID of the drop down item + */ + type: String, + }, + name: { + /** + * name of the drop down item + */ + type: String, + }, + }), + }, + showOnCard: { + /** + * should we show on the cards this custom field + */ + type: Boolean, + }, + automaticallyOnCard: { + /** + * should the custom fields automatically be added on cards? + */ + type: Boolean, + }, + showLabelOnMiniCard: { + /** + * should the label of the custom field be shown on minicards? + */ + type: Boolean, + }, + createdAt: { + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } }, - name: { - /** - * name of the drop down item - */ - type: String, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } }, - }), - }, - showOnCard: { - /** - * should we show on the cards this custom field - */ - type: Boolean, - }, - automaticallyOnCard: { - /** - * should the custom fields automatically be added on cards? - */ - type: Boolean, - }, - showLabelOnMiniCard: { - /** - * should the label of the custom field be shown on minicards? - */ - type: Boolean, - }, -})); + }, + }) +); CustomFields.mutations({ addBoard(boardId) { @@ -88,19 +114,28 @@ CustomFields.mutations({ CustomFields.allow({ insert(userId, doc) { - return allowIsAnyBoardMember(userId, Boards.find({ - _id: {$in: doc.boardIds}, - }).fetch()); + return allowIsAnyBoardMember( + userId, + Boards.find({ + _id: { $in: doc.boardIds }, + }).fetch() + ); }, update(userId, doc) { - return allowIsAnyBoardMember(userId, Boards.find({ - _id: {$in: doc.boardIds}, - }).fetch()); + return allowIsAnyBoardMember( + userId, + Boards.find({ + _id: { $in: doc.boardIds }, + }).fetch() + ); }, remove(userId, doc) { - return allowIsAnyBoardMember(userId, Boards.find({ - _id: {$in: doc.boardIds}, - }).fetch()); + return allowIsAnyBoardMember( + userId, + Boards.find({ + _id: { $in: doc.boardIds }, + }).fetch() + ); }, fetch: ['userId', 'boardIds'], }); @@ -108,7 +143,7 @@ CustomFields.allow({ // not sure if we need this? //CustomFields.hookOptions.after.update = { fetchPrevious: false }; -function customFieldCreation(userId, doc){ +function customFieldCreation(userId, doc) { Activities.insert({ userId, activityType: 'createCustomField', @@ -142,6 +177,7 @@ function customFieldEdit(userId, doc){ if (Meteor.isServer) { Meteor.startup(() => { + CustomFields._collection._ensureIndex({ modifiedAt: -1 }); CustomFields._collection._ensureIndex({ boardIds: 1 }); }); @@ -149,12 +185,17 @@ if (Meteor.isServer) { customFieldCreation(userId, doc); }); + CustomFields.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); + }); + CustomFields.before.update((userId, doc, fieldNames, modifier) => { if (_.contains(fieldNames, 'boardIds') && modifier.$pull) { Cards.update( - {boardId: modifier.$pull.boardIds, 'customFields._id': doc._id}, - {$pull: {'customFields': {'_id': doc._id}}}, - {multi: true} + { boardId: modifier.$pull.boardIds, 'customFields._id': doc._id }, + { $pull: { customFields: { _id: doc._id } } }, + { multi: true } ); customFieldEdit(userId, doc); Activities.remove({ @@ -180,9 +221,9 @@ if (Meteor.isServer) { }); Cards.update( - {boardId: {$in: doc.boardIds}, 'customFields._id': doc._id}, - {$pull: {'customFields': {'_id': doc._id}}}, - {multi: true} + { boardId: { $in: doc.boardIds }, 'customFields._id': doc._id }, + { $pull: { customFields: { _id: doc._id } } }, + { multi: true } ); }); } @@ -198,18 +239,23 @@ if (Meteor.isServer) { * name: string, * type: string}] */ - JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function (req, res) { - Authentication.checkUserId( req.userId); + JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function( + req, + res + ) { + Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; JsonRoutes.sendResult(res, { code: 200, - data: CustomFields.find({ boardIds: {$in: [paramBoardId]} }).map(function (cf) { - return { - _id: cf._id, - name: cf.name, - type: cf.type, - }; - }), + data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map( + function(cf) { + return { + _id: cf._id, + name: cf.name, + type: cf.type, + }; + } + ), }); }); @@ -221,15 +267,22 @@ if (Meteor.isServer) { * @param {string} customFieldId the ID of the custom field * @return_type CustomFields */ - JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const paramCustomFieldId = req.params.customFieldId; - JsonRoutes.sendResult(res, { - code: 200, - data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }), - }); - }); + JsonRoutes.add( + 'GET', + '/api/boards/:boardId/custom-fields/:customFieldId', + function(req, res) { + Authentication.checkUserId(req.userId); + const paramBoardId = req.params.boardId; + const paramCustomFieldId = req.params.customFieldId; + JsonRoutes.sendResult(res, { + code: 200, + data: CustomFields.findOne({ + _id: paramCustomFieldId, + boardIds: { $in: [paramBoardId] }, + }), + }); + } + ); /** * @operation new_custom_field @@ -244,8 +297,11 @@ if (Meteor.isServer) { * @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards? * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function (req, res) { - Authentication.checkUserId( req.userId); + JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function( + req, + res + ) { + Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const id = CustomFields.direct.insert({ name: req.body.name, @@ -254,10 +310,13 @@ if (Meteor.isServer) { showOnCard: req.body.showOnCard, automaticallyOnCard: req.body.automaticallyOnCard, showLabelOnMiniCard: req.body.showLabelOnMiniCard, - boardIds: {$in: [paramBoardId]}, + boardIds: { $in: [paramBoardId] }, }); - const customField = CustomFields.findOne({_id: id, boardIds: {$in: [paramBoardId]} }); + const customField = CustomFields.findOne({ + _id: id, + boardIds: { $in: [paramBoardId] }, + }); customFieldCreation(req.body.authorId, customField); JsonRoutes.sendResult(res, { @@ -278,16 +337,22 @@ if (Meteor.isServer) { * @param {string} customFieldId the ID of the custom field * @return_type {_id: string} */ - JsonRoutes.add('DELETE', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) { - Authentication.checkUserId( req.userId); - const paramBoardId = req.params.boardId; - const id = req.params.customFieldId; - CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: id, - }, - }); - }); + JsonRoutes.add( + 'DELETE', + '/api/boards/:boardId/custom-fields/:customFieldId', + function(req, res) { + Authentication.checkUserId(req.userId); + const paramBoardId = req.params.boardId; + const id = req.params.customFieldId; + CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + } + ); } + +export default CustomFields; diff --git a/models/integrations.js b/models/integrations.js index 65a7af63..bb36d6e8 100644 --- a/models/integrations.js +++ b/models/integrations.js @@ -3,75 +3,96 @@ Integrations = new Mongo.Collection('integrations'); /** * Integration with third-party applications */ -Integrations.attachSchema(new SimpleSchema({ - enabled: { - /** - * is the integration enabled? - */ - type: Boolean, - defaultValue: true, - }, - title: { - /** - * name of the integration - */ - type: String, - optional: true, - }, - type: { - /** - * type of the integratation (Default to 'outgoing-webhooks') - */ - type: String, - defaultValue: 'outgoing-webhooks', - }, - activities: { - /** - * activities the integration gets triggered (list) - */ - type: [String], - defaultValue: ['all'], - }, - url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex) - /** - * URL validation regex (https://mathiasbynens.be/demo/url-regex) - */ - type: String, - }, - token: { - /** - * token of the integration - */ - type: String, - optional: true, - }, - boardId: { - /** - * Board ID of the integration - */ - type: String, - }, - createdAt: { - /** - * Creation date of the integration - */ - type: Date, - denyUpdate: false, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return new Date(); - } else { - this.unset(); - } +Integrations.attachSchema( + new SimpleSchema({ + enabled: { + /** + * is the integration enabled? + */ + type: Boolean, + defaultValue: true, }, - }, - userId: { - /** - * user ID who created the interation - */ - type: String, - }, -})); + title: { + /** + * name of the integration + */ + type: String, + optional: true, + }, + type: { + /** + * type of the integratation (Default to 'outgoing-webhooks') + */ + type: String, + defaultValue: 'outgoing-webhooks', + }, + activities: { + /** + * activities the integration gets triggered (list) + */ + type: [String], + defaultValue: ['all'], + }, + url: { + // URL validation regex (https://mathiasbynens.be/demo/url-regex) + /** + * URL validation regex (https://mathiasbynens.be/demo/url-regex) + */ + type: String, + }, + token: { + /** + * token of the integration + */ + type: String, + optional: true, + }, + boardId: { + /** + * Board ID of the integration + */ + type: String, + }, + createdAt: { + /** + * Creation date of the integration + */ + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + userId: { + /** + * user ID who created the interation + */ + type: String, + }, + }) +); + +Integrations.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); Integrations.allow({ insert(userId, doc) { @@ -89,6 +110,7 @@ Integrations.allow({ //INTEGRATIONS REST API if (Meteor.isServer) { Meteor.startup(() => { + Integrations._collection._ensureIndex({ modifiedAt: -1 }); Integrations._collection._ensureIndex({ boardId: 1 }); }); @@ -99,18 +121,23 @@ if (Meteor.isServer) { * @param {string} boardId the board ID * @return_type [Integrations] */ - JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res) { + 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) { + const data = Integrations.find( + { boardId: paramBoardId }, + { fields: { token: 0 } } + ).map(function(doc) { return doc; }); - JsonRoutes.sendResult(res, {code: 200, data}); - } - catch (error) { + JsonRoutes.sendResult(res, { code: 200, data }); + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -126,7 +153,10 @@ if (Meteor.isServer) { * @param {string} intId the integration ID * @return_type Integrations */ - JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res) { + JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function( + req, + res + ) { try { const paramBoardId = req.params.boardId; const paramIntId = req.params.intId; @@ -134,10 +164,12 @@ if (Meteor.isServer) { JsonRoutes.sendResult(res, { code: 200, - data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }), + data: Integrations.findOne( + { _id: paramIntId, boardId: paramBoardId }, + { fields: { token: 0 } } + ), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -153,7 +185,10 @@ if (Meteor.isServer) { * @param {string} url the URL of the integration * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res) { + JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function( + req, + res + ) { try { const paramBoardId = req.params.boardId; Authentication.checkBoardAccess(req.userId, paramBoardId); @@ -170,8 +205,7 @@ if (Meteor.isServer) { _id: id, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -192,7 +226,10 @@ if (Meteor.isServer) { * @param {string} [activities] new list of activities of the integration * @return_type {_id: string} */ - JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res) { + JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function( + req, + res + ) { try { const paramBoardId = req.params.boardId; const paramIntId = req.params.intId; @@ -200,28 +237,38 @@ if (Meteor.isServer) { if (req.body.hasOwnProperty('enabled')) { const newEnabled = req.body.enabled; - Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, - {$set: {enabled: newEnabled}}); + 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}}); + 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}}); + 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}}); + 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}}); + Integrations.direct.update( + { _id: paramIntId, boardId: paramBoardId }, + { $set: { activities: newActivities } } + ); } JsonRoutes.sendResult(res, { @@ -230,8 +277,7 @@ if (Meteor.isServer) { _id: paramIntId, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -248,28 +294,36 @@ if (Meteor.isServer) { * @param {string} newActivities the activities to remove from the integration * @return_type Integrations */ - 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); + 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}}); + 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, - }); + 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, + }); + } } - }); + ); /** * @operation new_integration_activities @@ -280,28 +334,36 @@ if (Meteor.isServer) { * @param {string} newActivities the activities to add to the integration * @return_type Integrations */ - 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); + 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}}}); + 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, - }); + 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, + }); + } } - }); + ); /** * @operation delete_integration @@ -311,21 +373,23 @@ if (Meteor.isServer) { * @param {string} intId the integration ID * @return_type {_id: string} */ - JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res) { + 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}); + Integrations.direct.remove({ _id: paramIntId, boardId: paramBoardId }); JsonRoutes.sendResult(res, { code: 200, data: { _id: paramIntId, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -333,3 +397,5 @@ if (Meteor.isServer) { } }); } + +export default Integrations; diff --git a/models/invitationCodes.js b/models/invitationCodes.js index 53163f06..5cdfc744 100644 --- a/models/invitationCodes.js +++ b/models/invitationCodes.js @@ -1,45 +1,78 @@ InvitationCodes = new Mongo.Collection('invitation_codes'); -InvitationCodes.attachSchema(new SimpleSchema({ - code: { - type: String, - }, - email: { - type: String, - unique: true, - regEx: SimpleSchema.RegEx.Email, - }, - createdAt: { - type: Date, - denyUpdate: false, - }, - // always be the admin if only one admin - authorId: { - type: String, - }, - boardsToBeInvited: { - type: [String], - optional: true, - }, - valid: { - type: Boolean, - defaultValue: true, - }, -})); +InvitationCodes.attachSchema( + new SimpleSchema({ + code: { + type: String, + }, + email: { + type: String, + unique: true, + regEx: SimpleSchema.RegEx.Email, + }, + createdAt: { + type: Date, + denyUpdate: false, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + // always be the admin if only one admin + authorId: { + type: String, + }, + boardsToBeInvited: { + type: [String], + optional: true, + }, + valid: { + type: Boolean, + defaultValue: true, + }, + }) +); InvitationCodes.helpers({ - author(){ + author() { return Users.findOne(this.authorId); }, }); +InvitationCodes.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + // InvitationCodes.before.insert((userId, doc) => { // doc.createdAt = new Date(); // doc.authorId = userId; // }); if (Meteor.isServer) { + Meteor.startup(() => { + InvitationCodes._collection._ensureIndex({ modifiedAt: -1 }); + }); Boards.deny({ fetch: ['members'], }); } + +export default InvitationCodes; diff --git a/models/lists.js b/models/lists.js index 1a0910c2..6d77d7aa 100644 --- a/models/lists.js +++ b/models/lists.js @@ -3,125 +3,161 @@ Lists = new Mongo.Collection('lists'); /** * A list (column) in the Wekan board. */ -Lists.attachSchema(new SimpleSchema({ - title: { - /** - * the title of the list - */ - type: String, - }, - archived: { - /** - * is the list archived - */ - type: Boolean, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return false; - } +Lists.attachSchema( + new SimpleSchema({ + title: { + /** + * the title of the list + */ + type: String, }, - }, - boardId: { - /** - * the board associated to this list - */ - type: String, - }, - swimlaneId: { - /** - * the swimlane associated to this list. Used for templates - */ - type: String, - defaultValue: '', - }, - createdAt: { - /** - * creation date - */ - type: Date, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return new Date(); - } else { - this.unset(); - } + archived: { + /** + * is the list archived + */ + type: Boolean, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return false; + } + }, }, - }, - sort: { - /** - * is the list sorted - */ - type: Number, - decimal: true, - // XXX We should probably provide a default - optional: true, - }, - updatedAt: { - /** - * last update of the list - */ - type: Date, - optional: true, - autoValue() { // eslint-disable-line consistent-return - if (this.isUpdate) { - return new Date(); - } else { - this.unset(); - } + boardId: { + /** + * the board associated to this list + */ + type: String, }, - }, - wipLimit: { - /** - * WIP object, see below - */ - type: Object, - optional: true, - }, - 'wipLimit.value': { - /** - * value of the WIP - */ - type: Number, - decimal: false, - defaultValue: 1, - }, - 'wipLimit.enabled': { - /** - * is the WIP enabled - */ - type: Boolean, - defaultValue: false, - }, - 'wipLimit.soft': { - /** - * is the WIP a soft or hard requirement - */ - type: Boolean, - defaultValue: false, - }, - color: { - /** - * the color of the list - */ - type: String, - optional: true, - // silver is the default, so it is left out - allowedValues: [ - 'white', 'green', 'yellow', 'orange', 'red', 'purple', - 'blue', 'sky', 'lime', 'pink', 'black', - 'peachpuff', 'crimson', 'plum', 'darkgreen', - 'slateblue', 'magenta', 'gold', 'navy', 'gray', - 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', - ], - }, - type: { - /** - * The type of list - */ - type: String, - defaultValue: 'list', - }, -})); + swimlaneId: { + /** + * the swimlane associated to this list. Used for templates + */ + type: String, + defaultValue: '', + }, + createdAt: { + /** + * creation date + */ + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + sort: { + /** + * is the list sorted + */ + type: Number, + decimal: true, + // XXX We should probably provide a default + optional: true, + }, + updatedAt: { + /** + * last update of the list + */ + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isUpdate || this.isUpsert || this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + wipLimit: { + /** + * WIP object, see below + */ + type: Object, + optional: true, + }, + 'wipLimit.value': { + /** + * value of the WIP + */ + type: Number, + decimal: false, + defaultValue: 1, + }, + 'wipLimit.enabled': { + /** + * is the WIP enabled + */ + type: Boolean, + defaultValue: false, + }, + 'wipLimit.soft': { + /** + * is the WIP a soft or hard requirement + */ + type: Boolean, + defaultValue: false, + }, + color: { + /** + * the color of the list + */ + type: String, + optional: true, + // silver is the default, so it is left out + allowedValues: [ + 'white', + 'green', + 'yellow', + 'orange', + 'red', + 'purple', + 'blue', + 'sky', + 'lime', + 'pink', + 'black', + 'peachpuff', + 'crimson', + 'plum', + 'darkgreen', + 'slateblue', + 'magenta', + 'gold', + 'navy', + 'gray', + 'saddlebrown', + 'paleturquoise', + 'mistyrose', + 'indigo', + ], + }, + type: { + /** + * The type of list + */ + type: String, + defaultValue: 'list', + }, + }) +); Lists.allow({ insert(userId, doc) { @@ -172,10 +208,8 @@ Lists.helpers({ listId: this._id, archived: false, }; - if (swimlaneId) - selector.swimlaneId = swimlaneId; - return Cards.find(Filter.mongoSelector(selector), - { sort: ['sort'] }); + if (swimlaneId) selector.swimlaneId = swimlaneId; + return Cards.find(Filter.mongoSelector(selector), { sort: ['sort'] }); }, cardsUnfiltered(swimlaneId) { @@ -183,10 +217,8 @@ Lists.helpers({ listId: this._id, archived: false, }; - if (swimlaneId) - selector.swimlaneId = swimlaneId; - return Cards.find(selector, - { sort: ['sort'] }); + if (swimlaneId) selector.swimlaneId = swimlaneId; + return Cards.find(selector, { sort: ['sort'] }); }, allCards() { @@ -197,11 +229,12 @@ Lists.helpers({ return Boards.findOne(this.boardId); }, - getWipLimit(option){ + 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 + 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) { + } 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 @@ -209,8 +242,7 @@ Lists.helpers({ }, colorClass() { - if (this.color) - return this.color; + if (this.color) return this.color; return ''; }, @@ -219,7 +251,7 @@ Lists.helpers({ }, remove() { - Lists.remove({ _id: this._id}); + Lists.remove({ _id: this._id }); }, }); @@ -271,10 +303,10 @@ Lists.mutations({ }); Meteor.methods({ - applyWipLimit(listId, limit){ + applyWipLimit(listId, limit) { check(listId, String); check(limit, Number); - if(limit === 0){ + if (limit === 0) { limit = 1; } Lists.findOne({ _id: listId }).setWipLimit(limit); @@ -283,7 +315,7 @@ Meteor.methods({ enableWipLimit(listId) { check(listId, String); const list = Lists.findOne({ _id: listId }); - if(list.getWipLimit('value') === 0){ + if (list.getWipLimit('value') === 0) { list.setWipLimit(1); } list.toggleWipLimit(!list.getWipLimit('enabled')); @@ -300,6 +332,7 @@ Lists.hookOptions.after.update = { fetchPrevious: false }; if (Meteor.isServer) { Meteor.startup(() => { + Lists._collection._ensureIndex({ modifiedAt: -1 }); Lists._collection._ensureIndex({ boardId: 1 }); }); @@ -313,6 +346,11 @@ if (Meteor.isServer) { }); }); + Lists.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); + }); + Lists.before.remove((userId, doc) => { const cards = Cards.find({ listId: doc._id }); if (cards) { @@ -353,22 +391,23 @@ if (Meteor.isServer) { * @return_type [{_id: string, * title: string}] */ - JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) { + JsonRoutes.add('GET', '/api/boards/:boardId/lists', function(req, res) { try { const paramBoardId = req.params.boardId; - Authentication.checkBoardAccess( req.userId, paramBoardId); + 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, - }; - }), + data: Lists.find({ boardId: paramBoardId, archived: false }).map( + function(doc) { + return { + _id: doc._id, + title: doc.title, + }; + } + ), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -384,17 +423,23 @@ if (Meteor.isServer) { * @param {string} listId the List ID * @return_type Lists */ - JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res) { + 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); + Authentication.checkBoardAccess(req.userId, paramBoardId); JsonRoutes.sendResult(res, { code: 200, - data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }), + data: Lists.findOne({ + _id: paramListId, + boardId: paramBoardId, + archived: false, + }), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -410,9 +455,9 @@ if (Meteor.isServer) { * @param {string} title the title of the List * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res) { + JsonRoutes.add('POST', '/api/boards/:boardId/lists', function(req, res) { try { - Authentication.checkUserId( req.userId); + Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const board = Boards.findOne(paramBoardId); const id = Lists.insert({ @@ -426,8 +471,7 @@ if (Meteor.isServer) { _id: id, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -446,9 +490,12 @@ if (Meteor.isServer) { * @param {string} listId the ID of the list to remove * @return_type {_id: string} */ - JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res) { + JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function( + req, + res + ) { try { - Authentication.checkUserId( req.userId); + Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const paramListId = req.params.listId; Lists.remove({ _id: paramListId, boardId: paramBoardId }); @@ -458,13 +505,13 @@ if (Meteor.isServer) { _id: paramListId, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, }); } }); - } + +export default Lists; diff --git a/models/rules.js b/models/rules.js index 7d971980..76170b33 100644 --- a/models/rules.js +++ b/models/rules.js @@ -1,23 +1,51 @@ +import { Meteor } from 'meteor/meteor'; + Rules = new Mongo.Collection('rules'); -Rules.attachSchema(new SimpleSchema({ - title: { - type: String, - optional: false, - }, - triggerId: { - type: String, - optional: false, - }, - actionId: { - type: String, - optional: false, - }, - boardId: { - type: String, - optional: false, - }, -})); +Rules.attachSchema( + new SimpleSchema({ + title: { + type: String, + optional: false, + }, + triggerId: { + type: String, + optional: false, + }, + actionId: { + type: String, + optional: false, + }, + boardId: { + type: String, + optional: false, + }, + createdAt: { + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }) +); Rules.mutations({ rename(description) { @@ -26,15 +54,14 @@ Rules.mutations({ }); Rules.helpers({ - getAction(){ - return Actions.findOne({_id:this.actionId}); + getAction() { + return Actions.findOne({ _id: this.actionId }); }, - getTrigger(){ - return Triggers.findOne({_id:this.triggerId}); + getTrigger() { + return Triggers.findOne({ _id: this.triggerId }); }, }); - Rules.allow({ insert(userId, doc) { return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); @@ -46,3 +73,16 @@ Rules.allow({ return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); }, }); + +Rules.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + +if (Meteor.isServer) { + Meteor.startup(() => { + Rules._collection._ensureIndex({ modifiedAt: -1 }); + }); +} + +export default Rules; diff --git a/models/settings.js b/models/settings.js index e0f94fca..b97961eb 100644 --- a/models/settings.js +++ b/models/settings.js @@ -1,67 +1,85 @@ Settings = new Mongo.Collection('settings'); -Settings.attachSchema(new SimpleSchema({ - disableRegistration: { - type: Boolean, - }, - 'mailServer.username': { - type: String, - optional: true, - }, - 'mailServer.password': { - type: String, - optional: true, - }, - 'mailServer.host': { - type: String, - optional: true, - }, - 'mailServer.port': { - type: String, - optional: true, - }, - 'mailServer.enableTLS': { - type: Boolean, - optional: true, - }, - 'mailServer.from': { - type: String, - optional: true, - }, - productName: { - type: String, - optional: true, - }, - customHTMLafterBodyStart: { - type: String, - optional: true, - }, - customHTMLbeforeBodyEnd: { - type: String, - optional: true, - }, - displayAuthenticationMethod: { - type: Boolean, - optional: true, - }, - defaultAuthenticationMethod: { - type: String, - optional: false, - }, - hideLogo: { - type: Boolean, - optional: true, - }, - createdAt: { - type: Date, - denyUpdate: true, - }, - modifiedAt: { - type: Date, - }, -})); +Settings.attachSchema( + new SimpleSchema({ + disableRegistration: { + type: Boolean, + }, + 'mailServer.username': { + type: String, + optional: true, + }, + 'mailServer.password': { + type: String, + optional: true, + }, + 'mailServer.host': { + type: String, + optional: true, + }, + 'mailServer.port': { + type: String, + optional: true, + }, + 'mailServer.enableTLS': { + type: Boolean, + optional: true, + }, + 'mailServer.from': { + type: String, + optional: true, + }, + productName: { + type: String, + optional: true, + }, + customHTMLafterBodyStart: { + type: String, + optional: true, + }, + customHTMLbeforeBodyEnd: { + type: String, + optional: true, + }, + displayAuthenticationMethod: { + type: Boolean, + optional: true, + }, + defaultAuthenticationMethod: { + type: String, + optional: false, + }, + hideLogo: { + type: Boolean, + optional: true, + }, + createdAt: { + type: Date, + denyUpdate: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }) +); Settings.helpers({ - mailUrl () { + mailUrl() { if (!this.mailServer.host) { return null; } @@ -69,7 +87,9 @@ Settings.helpers({ if (!this.mailServer.username && !this.mailServer.password) { return `${protocol}${this.mailServer.host}:${this.mailServer.port}/`; } - return `${protocol}${this.mailServer.username}:${encodeURIComponent(this.mailServer.password)}@${this.mailServer.host}:${this.mailServer.port}/`; + return `${protocol}${this.mailServer.username}:${encodeURIComponent( + this.mailServer.password + )}@${this.mailServer.host}:${this.mailServer.port}/`; }, }); Settings.allow({ @@ -86,50 +106,75 @@ Settings.before.update((userId, doc, fieldNames, modifier) => { if (Meteor.isServer) { Meteor.startup(() => { + Settings._collection._ensureIndex({ modifiedAt: -1 }); const setting = Settings.findOne({}); - if(!setting){ + if (!setting) { const now = new Date(); - const domain = process.env.ROOT_URL.match(/\/\/(?:www\.)?(.*)?(?:\/)?/)[1]; + const domain = process.env.ROOT_URL.match( + /\/\/(?:www\.)?(.*)?(?:\/)?/ + )[1]; const from = `Boards Support `; - const defaultSetting = {disableRegistration: false, mailServer: { - username: '', password: '', host: '', port: '', enableTLS: false, from, - }, createdAt: now, modifiedAt: now, displayAuthenticationMethod: true, - defaultAuthenticationMethod: 'password'}; + const defaultSetting = { + disableRegistration: false, + mailServer: { + username: '', + password: '', + host: '', + port: '', + enableTLS: false, + from, + }, + createdAt: now, + modifiedAt: now, + displayAuthenticationMethod: true, + defaultAuthenticationMethod: 'password', + }; Settings.insert(defaultSetting); } const newSetting = Settings.findOne(); if (!process.env.MAIL_URL && newSetting.mailUrl()) process.env.MAIL_URL = newSetting.mailUrl(); - Accounts.emailTemplates.from = process.env.MAIL_FROM ? process.env.MAIL_FROM : newSetting.mailServer.from; + Accounts.emailTemplates.from = process.env.MAIL_FROM + ? process.env.MAIL_FROM + : newSetting.mailServer.from; }); Settings.after.update((userId, doc, fieldNames) => { // assign new values to mail-from & MAIL_URL in environment if (_.contains(fieldNames, 'mailServer') && doc.mailServer.host) { const protocol = doc.mailServer.enableTLS ? 'smtps://' : 'smtp://'; if (!doc.mailServer.username && !doc.mailServer.password) { - process.env.MAIL_URL = `${protocol}${doc.mailServer.host}:${doc.mailServer.port}/`; + process.env.MAIL_URL = `${protocol}${doc.mailServer.host}:${ + doc.mailServer.port + }/`; } else { - process.env.MAIL_URL = `${protocol}${doc.mailServer.username}:${encodeURIComponent(doc.mailServer.password)}@${doc.mailServer.host}:${doc.mailServer.port}/`; + process.env.MAIL_URL = `${protocol}${ + doc.mailServer.username + }:${encodeURIComponent(doc.mailServer.password)}@${ + doc.mailServer.host + }:${doc.mailServer.port}/`; } Accounts.emailTemplates.from = doc.mailServer.from; } }); - function getRandomNum (min, max) { + function getRandomNum(min, max) { const range = max - min; const rand = Math.random(); - return (min + Math.round(rand * range)); + return min + Math.round(rand * range); } - function getEnvVar(name){ + function getEnvVar(name) { const value = process.env[name]; - if (value){ + if (value) { return value; } - throw new Meteor.Error(['var-not-exist', `The environment variable ${name} does not exist`]); + throw new Meteor.Error([ + 'var-not-exist', + `The environment variable ${name} does not exist`, + ]); } - function sendInvitationEmail (_id){ + function sendInvitationEmail(_id) { const icode = InvitationCodes.findOne(_id); const author = Users.findOne(Meteor.userId()); try { @@ -172,30 +217,47 @@ if (Meteor.isServer) { check(boards, [String]); const user = Users.findOne(Meteor.userId()); - if(!user.isAdmin){ + if (!user.isAdmin) { throw new Meteor.Error('not-allowed'); } emails.forEach((email) => { if (email && SimpleSchema.RegEx.Email.test(email)) { // Checks if the email is already link to an account. - const userExist = Users.findOne({email}); - if (userExist){ - throw new Meteor.Error('user-exist', `The user with the email ${email} has already an account.`); + const userExist = Users.findOne({ email }); + if (userExist) { + throw new Meteor.Error( + 'user-exist', + `The user with the email ${email} has already an account.` + ); } // Checks if the email is already link to an invitation. - const invitation = InvitationCodes.findOne({email}); - if (invitation){ - InvitationCodes.update(invitation, {$set : {boardsToBeInvited: boards}}); + const invitation = InvitationCodes.findOne({ email }); + if (invitation) { + InvitationCodes.update(invitation, { + $set: { boardsToBeInvited: boards }, + }); sendInvitationEmail(invitation._id); - }else { + } else { const code = getRandomNum(100000, 999999); - InvitationCodes.insert({code, email, boardsToBeInvited: boards, createdAt: new Date(), authorId: Meteor.userId()}, function(err, _id){ - if (!err && _id) { - sendInvitationEmail(_id); - } else { - throw new Meteor.Error('invitation-generated-fail', err.message); + InvitationCodes.insert( + { + code, + email, + boardsToBeInvited: boards, + createdAt: new Date(), + authorId: Meteor.userId(), + }, + function(err, _id) { + if (!err && _id) { + sendInvitationEmail(_id); + } else { + throw new Meteor.Error( + 'invitation-generated-fail', + err.message + ); + } } - }); + ); } } }); @@ -215,11 +277,15 @@ if (Meteor.isServer) { 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}), + 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); + } catch ({ message }) { + throw new Meteor.Error( + 'email-fail', + `${TAPi18n.__('email-fail-text', { lng: lang })}: ${message}`, + message + ); } return { message: 'email-sent', @@ -227,7 +293,7 @@ if (Meteor.isServer) { }; }, - getCustomUI(){ + getCustomUI() { const setting = Settings.findOne({}); if (!setting.productName) { return { @@ -240,7 +306,7 @@ if (Meteor.isServer) { } }, - getMatomoConf(){ + getMatomoConf() { return { address: getEnvVar('MATOMO_ADDRESS'), siteId: getEnvVar('MATOMO_SITE_ID'), @@ -275,3 +341,5 @@ if (Meteor.isServer) { }, }); } + +export default Settings; diff --git a/models/swimlanes.js b/models/swimlanes.js index 9a53d116..82f73f79 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -3,89 +3,125 @@ Swimlanes = new Mongo.Collection('swimlanes'); /** * A swimlane is an line in the kaban board. */ -Swimlanes.attachSchema(new SimpleSchema({ - title: { - /** - * the title of the swimlane - */ - type: String, - }, - archived: { - /** - * is the swimlane archived? - */ - type: Boolean, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return false; - } +Swimlanes.attachSchema( + new SimpleSchema({ + title: { + /** + * the title of the swimlane + */ + type: String, }, - }, - boardId: { - /** - * the ID of the board the swimlane is attached to - */ - type: String, - }, - createdAt: { - /** - * creation date of the swimlane - */ - type: Date, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return new Date(); - } else { - this.unset(); - } + archived: { + /** + * is the swimlane archived? + */ + type: Boolean, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return false; + } + }, }, - }, - sort: { - /** - * the sort value of the swimlane - */ - type: Number, - decimal: true, - // XXX We should probably provide a default - optional: true, - }, - color: { - /** - * the color of the swimlane - */ - type: String, - optional: true, - // silver is the default, so it is left out - allowedValues: [ - 'white', 'green', 'yellow', 'orange', 'red', 'purple', - 'blue', 'sky', 'lime', 'pink', 'black', - 'peachpuff', 'crimson', 'plum', 'darkgreen', - 'slateblue', 'magenta', 'gold', 'navy', 'gray', - 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', - ], - }, - updatedAt: { - /** - * when was the swimlane last edited - */ - type: Date, - optional: true, - autoValue() { // eslint-disable-line consistent-return - if (this.isUpdate) { - return new Date(); - } else { - this.unset(); - } + boardId: { + /** + * the ID of the board the swimlane is attached to + */ + type: String, }, - }, - type: { - /** - * The type of swimlane - */ - type: String, - defaultValue: 'swimlane', - }, -})); + createdAt: { + /** + * creation date of the swimlane + */ + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + sort: { + /** + * the sort value of the swimlane + */ + type: Number, + decimal: true, + // XXX We should probably provide a default + optional: true, + }, + color: { + /** + * the color of the swimlane + */ + type: String, + optional: true, + // silver is the default, so it is left out + allowedValues: [ + 'white', + 'green', + 'yellow', + 'orange', + 'red', + 'purple', + 'blue', + 'sky', + 'lime', + 'pink', + 'black', + 'peachpuff', + 'crimson', + 'plum', + 'darkgreen', + 'slateblue', + 'magenta', + 'gold', + 'navy', + 'gray', + 'saddlebrown', + 'paleturquoise', + 'mistyrose', + 'indigo', + ], + }, + updatedAt: { + /** + * when was the swimlane last edited + */ + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isUpdate || this.isUpsert || this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + type: { + /** + * The type of swimlane + */ + type: String, + defaultValue: 'swimlane', + }, + }) +); Swimlanes.allow({ insert(userId, doc) { @@ -109,7 +145,7 @@ Swimlanes.helpers({ const _id = Swimlanes.insert(this); const query = { - swimlaneId: {$in: [oldId, '']}, + swimlaneId: { $in: [oldId, ''] }, archived: false, }; if (oldBoardId) { @@ -126,18 +162,24 @@ Swimlanes.helpers({ }, cards() { - return Cards.find(Filter.mongoSelector({ - swimlaneId: this._id, - archived: false, - }), { sort: ['sort'] }); + return Cards.find( + Filter.mongoSelector({ + swimlaneId: this._id, + archived: false, + }), + { sort: ['sort'] } + ); }, lists() { - return Lists.find({ - boardId: this.boardId, - swimlaneId: {$in: [this._id, '']}, - archived: false, - }, { sort: ['sort'] }); + return Lists.find( + { + boardId: this.boardId, + swimlaneId: { $in: [this._id, ''] }, + archived: false, + }, + { sort: ['sort'] } + ); }, myLists() { @@ -153,8 +195,7 @@ Swimlanes.helpers({ }, colorClass() { - if (this.color) - return this.color; + if (this.color) return this.color; return ''; }, @@ -182,7 +223,7 @@ Swimlanes.helpers({ }, remove() { - Swimlanes.remove({ _id: this._id}); + Swimlanes.remove({ _id: this._id }); }, }); @@ -221,10 +262,16 @@ Swimlanes.mutations({ }, }); +Swimlanes.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + Swimlanes.hookOptions.after.update = { fetchPrevious: false }; if (Meteor.isServer) { Meteor.startup(() => { + Swimlanes._collection._ensureIndex({ modifiedAt: -1 }); Swimlanes._collection._ensureIndex({ boardId: 1 }); }); @@ -239,18 +286,21 @@ if (Meteor.isServer) { }); Swimlanes.before.remove(function(userId, doc) { - const lists = Lists.find({ - boardId: doc.boardId, - swimlaneId: {$in: [doc._id, '']}, - archived: false, - }, { sort: ['sort'] }); + const lists = Lists.find( + { + boardId: doc.boardId, + swimlaneId: { $in: [doc._id, ''] }, + archived: false, + }, + { sort: ['sort'] } + ); if (lists.count() < 2) { lists.forEach((list) => { list.remove(); }); } else { - Cards.remove({swimlaneId: doc._id}); + Cards.remove({ swimlaneId: doc._id }); } Activities.insert({ @@ -287,22 +337,23 @@ if (Meteor.isServer) { * @return_type [{_id: string, * title: string}] */ - JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) { + JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) { try { const paramBoardId = req.params.boardId; - Authentication.checkBoardAccess( req.userId, paramBoardId); + 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, - }; - }), + data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map( + function(doc) { + return { + _id: doc._id, + title: doc.title, + }; + } + ), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -319,17 +370,23 @@ if (Meteor.isServer) { * @param {string} swimlaneId the ID of the swimlane * @return_type Swimlanes */ - JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) { + 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); + Authentication.checkBoardAccess(req.userId, paramBoardId); JsonRoutes.sendResult(res, { code: 200, - data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }), + data: Swimlanes.findOne({ + _id: paramSwimlaneId, + boardId: paramBoardId, + archived: false, + }), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -346,9 +403,9 @@ if (Meteor.isServer) { * @param {string} title the new title of the swimlane * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res) { + JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) { try { - Authentication.checkUserId( req.userId); + Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const board = Boards.findOne(paramBoardId); const id = Swimlanes.insert({ @@ -362,8 +419,7 @@ if (Meteor.isServer) { _id: id, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -382,25 +438,29 @@ if (Meteor.isServer) { * @param {string} swimlaneId the ID of the swimlane * @return_type {_id: string} */ - 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, - }); + 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, + }); + } } - }); - + ); } + +export default Swimlanes; diff --git a/models/triggers.js b/models/triggers.js index 15982b6e..8f2448c4 100644 --- a/models/triggers.js +++ b/models/triggers.js @@ -1,3 +1,5 @@ +import { Meteor } from 'meteor/meteor'; + Triggers = new Mongo.Collection('triggers'); Triggers.mutations({ @@ -23,7 +25,6 @@ Triggers.allow({ }); Triggers.helpers({ - description() { return this.desc; }, @@ -56,3 +57,16 @@ Triggers.helpers({ return cardLabels; }, }); + +Triggers.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + +if (Meteor.isServer) { + Meteor.startup(() => { + Triggers._collection._ensureIndex({ modifiedAt: -1 }); + }); +} + +export default Triggers; diff --git a/models/unsavedEdits.js b/models/unsavedEdits.js index d4f3616a..122b2cd2 100644 --- a/models/unsavedEdits.js +++ b/models/unsavedEdits.js @@ -2,31 +2,66 @@ // `UnsavedEdits` API on the client. UnsavedEditCollection = new Mongo.Collection('unsaved-edits'); -UnsavedEditCollection.attachSchema(new SimpleSchema({ - fieldName: { - type: String, - }, - docId: { - type: String, - }, - value: { - type: String, - }, - userId: { - type: String, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return this.userId; - } +UnsavedEditCollection.attachSchema( + new SimpleSchema({ + fieldName: { + type: String, }, - }, -})); + docId: { + type: String, + }, + value: { + type: String, + }, + userId: { + type: String, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return this.userId; + } + }, + }, + createdAt: { + type: Date, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }) +); + +UnsavedEditCollection.before.update( + (userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); + } +); if (Meteor.isServer) { function isAuthor(userId, doc, fieldNames = []) { return userId === doc.userId && fieldNames.indexOf('userId') === -1; } Meteor.startup(() => { + UnsavedEditCollection._collection._ensureIndex({ modifiedAt: -1 }); UnsavedEditCollection._collection._ensureIndex({ userId: 1 }); }); UnsavedEditCollection.allow({ @@ -36,3 +71,5 @@ if (Meteor.isServer) { fetch: ['userId'], }); } + +export default UnsavedEditCollection; diff --git a/models/users.js b/models/users.js index 5f949c80..306193aa 100644 --- a/models/users.js +++ b/models/users.js @@ -1,237 +1,254 @@ // Sandstorm context is detected using the METEOR_SETTINGS environment variable // in the package definition. -const isSandstorm = Meteor.settings && Meteor.settings.public && - Meteor.settings.public.sandstorm; +const isSandstorm = + Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm; Users = Meteor.users; /** * A User in wekan */ -Users.attachSchema(new SimpleSchema({ - username: { - /** - * the username of the user - */ - type: String, - optional: true, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - const name = this.field('profile.fullname'); - if (name.isSet) { - return name.value.toLowerCase().replace(/\s/g, ''); +Users.attachSchema( + new SimpleSchema({ + username: { + /** + * the username of the user + */ + type: String, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + const name = this.field('profile.fullname'); + if (name.isSet) { + return name.value.toLowerCase().replace(/\s/g, ''); + } } - } + }, }, - }, - emails: { - /** - * the list of emails attached to a user - */ - type: [Object], - optional: true, - }, - 'emails.$.address': { - /** - * The email address - */ - type: String, - regEx: SimpleSchema.RegEx.Email, - }, - 'emails.$.verified': { - /** - * Has the email been verified - */ - type: Boolean, - }, - createdAt: { - /** - * creation date of the user - */ - type: Date, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return new Date(); - } else { - this.unset(); - } + emails: { + /** + * the list of emails attached to a user + */ + type: [Object], + optional: true, }, - }, - profile: { - /** - * profile settings - */ - type: Object, - optional: true, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert && !this.isSet) { - return { - boardView: 'board-view-lists', - }; - } + 'emails.$.address': { + /** + * The email address + */ + type: String, + regEx: SimpleSchema.RegEx.Email, }, - }, - 'profile.avatarUrl': { - /** - * URL of the avatar of the user - */ - type: String, - optional: true, - }, - 'profile.emailBuffer': { - /** - * list of email buffers of the user - */ - type: [String], - optional: true, - }, - 'profile.fullname': { - /** - * full name of the user - */ - type: String, - optional: true, - }, - 'profile.hiddenSystemMessages': { - /** - * does the user wants to hide system messages? - */ - type: Boolean, - optional: true, - }, - 'profile.initials': { - /** - * initials of the user - */ - type: String, - optional: true, - }, - 'profile.invitedBoards': { - /** - * board IDs the user has been invited to - */ - type: [String], - optional: true, - }, - 'profile.language': { - /** - * language of the user - */ - type: String, - optional: true, - }, - 'profile.notifications': { - /** - * enabled notifications for the user - */ - type: [String], - optional: true, - }, - 'profile.showCardsCountAt': { - /** - * showCardCountAt field of the user - */ - type: Number, - optional: true, - }, - 'profile.starredBoards': { - /** - * list of starred board IDs - */ - type: [String], - optional: true, - }, - 'profile.icode': { - /** - * icode - */ - type: String, - optional: true, - }, - 'profile.boardView': { - /** - * boardView field of the user - */ - type: String, - optional: true, - allowedValues: [ - 'board-view-lists', - 'board-view-swimlanes', - 'board-view-cal', - ], - }, - 'profile.templatesBoardId': { - /** - * Reference to the templates board - */ - type: String, - defaultValue: '', - }, - 'profile.cardTemplatesSwimlaneId': { - /** - * Reference to the card templates swimlane Id - */ - type: String, - defaultValue: '', - }, - 'profile.listTemplatesSwimlaneId': { - /** - * Reference to the list templates swimlane Id - */ - type: String, - defaultValue: '', - }, - 'profile.boardTemplatesSwimlaneId': { - /** - * Reference to the board templates swimlane Id - */ - type: String, - defaultValue: '', - }, - services: { - /** - * services field of the user - */ - type: Object, - optional: true, - blackbox: true, - }, - heartbeat: { - /** - * last time the user has been seen - */ - type: Date, - optional: true, - }, - isAdmin: { - /** - * is the user an admin of the board? - */ - type: Boolean, - optional: true, - }, - createdThroughApi: { - /** - * was the user created through the API? - */ - type: Boolean, - optional: true, - }, - loginDisabled: { - /** - * loginDisabled field of the user - */ - type: Boolean, - optional: true, - }, - 'authenticationMethod': { - /** - * authentication method of the user - */ - type: String, - optional: false, - defaultValue: 'password', - }, -})); + 'emails.$.verified': { + /** + * Has the email been verified + */ + type: Boolean, + }, + createdAt: { + /** + * creation date of the user + */ + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + profile: { + /** + * profile settings + */ + type: Object, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return { + boardView: 'board-view-lists', + }; + } + }, + }, + 'profile.avatarUrl': { + /** + * URL of the avatar of the user + */ + type: String, + optional: true, + }, + 'profile.emailBuffer': { + /** + * list of email buffers of the user + */ + type: [String], + optional: true, + }, + 'profile.fullname': { + /** + * full name of the user + */ + type: String, + optional: true, + }, + 'profile.hiddenSystemMessages': { + /** + * does the user wants to hide system messages? + */ + type: Boolean, + optional: true, + }, + 'profile.initials': { + /** + * initials of the user + */ + type: String, + optional: true, + }, + 'profile.invitedBoards': { + /** + * board IDs the user has been invited to + */ + type: [String], + optional: true, + }, + 'profile.language': { + /** + * language of the user + */ + type: String, + optional: true, + }, + 'profile.notifications': { + /** + * enabled notifications for the user + */ + type: [String], + optional: true, + }, + 'profile.showCardsCountAt': { + /** + * showCardCountAt field of the user + */ + type: Number, + optional: true, + }, + 'profile.starredBoards': { + /** + * list of starred board IDs + */ + type: [String], + optional: true, + }, + 'profile.icode': { + /** + * icode + */ + type: String, + optional: true, + }, + 'profile.boardView': { + /** + * boardView field of the user + */ + type: String, + optional: true, + allowedValues: [ + 'board-view-lists', + 'board-view-swimlanes', + 'board-view-cal', + ], + }, + 'profile.templatesBoardId': { + /** + * Reference to the templates board + */ + type: String, + defaultValue: '', + }, + 'profile.cardTemplatesSwimlaneId': { + /** + * Reference to the card templates swimlane Id + */ + type: String, + defaultValue: '', + }, + 'profile.listTemplatesSwimlaneId': { + /** + * Reference to the list templates swimlane Id + */ + type: String, + defaultValue: '', + }, + 'profile.boardTemplatesSwimlaneId': { + /** + * Reference to the board templates swimlane Id + */ + type: String, + defaultValue: '', + }, + services: { + /** + * services field of the user + */ + type: Object, + optional: true, + blackbox: true, + }, + heartbeat: { + /** + * last time the user has been seen + */ + type: Date, + optional: true, + }, + isAdmin: { + /** + * is the user an admin of the board? + */ + type: Boolean, + optional: true, + }, + createdThroughApi: { + /** + * was the user created through the API? + */ + type: Boolean, + optional: true, + }, + loginDisabled: { + /** + * loginDisabled field of the user + */ + type: Boolean, + optional: true, + }, + authenticationMethod: { + /** + * authentication method of the user + */ + type: String, + optional: false, + defaultValue: 'password', + }, + }) +); Users.allow({ update(userId) { @@ -240,7 +257,10 @@ Users.allow({ }, remove(userId, doc) { const adminsNumber = Users.find({ isAdmin: true }).count(); - const { isAdmin } = Users.findOne({ _id: userId }, { fields: { 'isAdmin': 1 } }); + const { isAdmin } = Users.findOne( + { _id: userId }, + { fields: { isAdmin: 1 } } + ); // Prevents remove of the only one administrator if (adminsNumber === 1 && isAdmin && userId === doc._id) { @@ -270,7 +290,9 @@ if (Meteor.isClient) { isNotNoComments() { const board = Boards.findOne(Session.get('currentBoard')); - return board && board.hasMember(this._id) && !board.hasNoComments(this._id); + return ( + board && board.hasMember(this._id) && !board.hasNoComments(this._id) + ); }, isNoComments() { @@ -280,7 +302,9 @@ if (Meteor.isClient) { isNotCommentOnly() { const board = Boards.findOne(Session.get('currentBoard')); - return board && board.hasMember(this._id) && !board.hasCommentOnly(this._id); + return ( + board && board.hasMember(this._id) && !board.hasCommentOnly(this._id) + ); }, isCommentOnly() { @@ -301,32 +325,32 @@ Users.helpers({ }, 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); }, @@ -336,20 +360,20 @@ Users.helpers({ }, getEmailBuffer() { - const {emailBuffer = []} = this.profile || {}; + const { emailBuffer = [] } = this.profile || {}; return emailBuffer; }, getInitials() { const profile = this.profile || {}; - if (profile.initials) - return profile.initials; - + if (profile.initials) return profile.initials; else if (profile.fullname) { - return profile.fullname.split(/\s+/).reduce((memo, word) => { - return memo + word[0]; - }, '').toUpperCase(); - + return profile.fullname + .split(/\s+/) + .reduce((memo, word) => { + return memo + word[0]; + }, '') + .toUpperCase(); } else { return this.username[0].toUpperCase(); } @@ -379,7 +403,7 @@ Users.helpers({ }, remove() { - User.remove({ _id: this._id}); + User.remove({ _id: this._id }); }, }); @@ -426,10 +450,8 @@ Users.mutations({ }, toggleTag(tag) { - if (this.hasTag(tag)) - this.removeTag(tag); - else - this.addTag(tag); + if (this.hasTag(tag)) this.removeTag(tag); + else this.addTag(tag); }, toggleSystem(value = false) { @@ -473,16 +495,16 @@ 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 : { + $set: { 'profile.boardView': view, }, }; @@ -492,11 +514,11 @@ Users.mutations({ Meteor.methods({ 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(userId, {$set: {username}}); + Users.update(userId, { $set: { username } }); } }, toggleSystemMessages() { @@ -509,16 +531,21 @@ Meteor.methods({ }, 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(userId, { $set: { - emails: [{ - address: email, - verified: false, - }], + emails: [ + { + address: email, + verified: false, + }, + ], }, }); } @@ -533,7 +560,7 @@ Meteor.methods({ setPassword(newPassword, userId) { check(userId, String); check(newPassword, String); - if(Meteor.user().isAdmin){ + if (Meteor.user().isAdmin) { Accounts.setPassword(userId, newPassword); } }, @@ -548,12 +575,13 @@ if (Meteor.isServer) { const inviter = Meteor.user(); const board = Boards.findOne(boardId); - const allowInvite = inviter && + const allowInvite = + inviter && 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(); @@ -561,19 +589,21 @@ 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'); + if (user._id === inviter._id) + throw new Meteor.Error('error-user-notAllowSelf'); } else { if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist'); - if (Settings.findOne().disableRegistration) throw new Meteor.Error('error-user-notCreated'); + if (Settings.findOne().disableRegistration) + throw new Meteor.Error('error-user-notCreated'); // 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) { @@ -607,7 +637,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) => { @@ -621,14 +651,22 @@ if (Meteor.isServer) { const email = user.services.oidc.email.toLowerCase(); user.username = user.services.oidc.username; user.emails = [{ address: email, verified: true }]; - const initials = user.services.oidc.fullname.match(/\b[a-zA-Z]/g).join('').toUpperCase(); - user.profile = { initials, fullname: user.services.oidc.fullname, boardView: 'board-view-lists' }; + const initials = user.services.oidc.fullname + .match(/\b[a-zA-Z]/g) + .join('') + .toUpperCase(); + user.profile = { + initials, + fullname: user.services.oidc.fullname, + boardView: 'board-view-lists', + }; user.authenticationMethod = 'oauth2'; // see if any existing user has this email address or username, otherwise create new - const existingUser = Meteor.users.findOne({$or: [{'emails.address': email}, {'username':user.username}]}); - if (!existingUser) - return user; + const existingUser = Meteor.users.findOne({ + $or: [{ 'emails.address': email }, { username: user.username }], + }); + if (!existingUser) return user; // copy across new service info const service = _.keys(user.services)[0]; @@ -638,7 +676,7 @@ if (Meteor.isServer) { existingUser.profile = user.profile; existingUser.authenticationMethod = user.authenticationMethod; - Meteor.users.remove({_id: existingUser._id}); // remove existing record + Meteor.users.remove({ _id: existingUser._id }); // remove existing record return existingUser; } @@ -660,7 +698,10 @@ if (Meteor.isServer) { } if (!options || !options.profile) { - throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required'); + throw new Meteor.Error( + 'error-invitation-code-blank', + 'The invitation code is required' + ); } const invitationCode = InvitationCodes.findOne({ code: options.profile.invitationcode, @@ -668,26 +709,41 @@ if (Meteor.isServer) { valid: true, }); if (!invitationCode) { - throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist'); + 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'; // Deletes the invitation code after the user was created successfully. - setTimeout(Meteor.bindEnvironment(() => { - InvitationCodes.remove({'_id': invitationCode._id}); - }), 200); + setTimeout( + Meteor.bindEnvironment(() => { + InvitationCodes.remove({ _id: invitationCode._id }); + }), + 200 + ); return user; } }); } +Users.before.update((userId, doc, fieldNames, modifier, options) => { + modifier.$set = modifier.$set || {}; + modifier.$set.modifiedAt = Date.now(); +}); + if (Meteor.isServer) { // Let mongoDB ensure username unicity Meteor.startup(() => { - Users._collection._ensureIndex({ - username: 1, - }, {unique: true}); + Users._collection._ensureIndex({ modifiedAt: -1 }); + Users._collection._ensureIndex( + { + username: 1, + }, + { unique: true } + ); }); // OLD WAY THIS CODE DID WORK: When user is last admin of board, @@ -712,11 +768,10 @@ if (Meteor.isServer) { // counter. // We need to run this code on the server only, otherwise the incrementation // will be done twice. - Users.after.update(function (userId, user, fieldNames) { + Users.after.update(function(userId, user, fieldNames) { // The `starredBoards` list is hosted on the `profile` field. If this // field hasn't been modificated we don't need to run this hook. - if (!_.contains(fieldNames, 'profile')) - return; + if (!_.contains(fieldNames, 'profile')) return; // To calculate a diff of board starred ids, we get both the previous // and the newly board ids list @@ -732,7 +787,7 @@ 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 } }); }); } @@ -754,7 +809,7 @@ if (Meteor.isServer) { }; fakeUserId.withValue(doc._id, () => { - /* + /* // Insert the Welcome Board Boards.insert({ title: TAPi18n.__('welcome-board'), @@ -773,57 +828,76 @@ if (Meteor.isServer) { }); */ - Boards.insert({ - title: TAPi18n.__('templates'), - permission: 'private', - type: 'template-container', - }, fakeUser, (err, boardId) => { - - // Insert the reference to our templates board - Users.update(fakeUserId.get(), {$set: {'profile.templatesBoardId': boardId}}); - - // Insert the card templates swimlane - Swimlanes.insert({ - title: TAPi18n.__('card-templates-swimlane'), - boardId, - sort: 1, - type: 'template-container', - }, fakeUser, (err, swimlaneId) => { - - // Insert the reference to out card templates swimlane - Users.update(fakeUserId.get(), {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); - }); - - // Insert the list templates swimlane - Swimlanes.insert({ - title: TAPi18n.__('list-templates-swimlane'), - boardId, - sort: 2, - type: 'template-container', - }, fakeUser, (err, swimlaneId) => { - - // Insert the reference to out list templates swimlane - Users.update(fakeUserId.get(), {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); - }); - - // Insert the board templates swimlane - Swimlanes.insert({ - title: TAPi18n.__('board-templates-swimlane'), - boardId, - sort: 3, + Boards.insert( + { + title: TAPi18n.__('templates'), + permission: 'private', type: 'template-container', - }, fakeUser, (err, swimlaneId) => { - - // Insert the reference to out board templates swimlane - Users.update(fakeUserId.get(), {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}}); - }); - }); + }, + fakeUser, + (err, boardId) => { + // Insert the reference to our templates board + Users.update(fakeUserId.get(), { + $set: { 'profile.templatesBoardId': boardId }, + }); + + // Insert the card templates swimlane + Swimlanes.insert( + { + title: TAPi18n.__('card-templates-swimlane'), + boardId, + sort: 1, + type: 'template-container', + }, + fakeUser, + (err, swimlaneId) => { + // Insert the reference to out card templates swimlane + Users.update(fakeUserId.get(), { + $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId }, + }); + } + ); + + // Insert the list templates swimlane + Swimlanes.insert( + { + title: TAPi18n.__('list-templates-swimlane'), + boardId, + sort: 2, + type: 'template-container', + }, + fakeUser, + (err, swimlaneId) => { + // Insert the reference to out list templates swimlane + Users.update(fakeUserId.get(), { + $set: { 'profile.listTemplatesSwimlaneId': swimlaneId }, + }); + } + ); + + // Insert the board templates swimlane + Swimlanes.insert( + { + title: TAPi18n.__('board-templates-swimlane'), + boardId, + sort: 3, + type: 'template-container', + }, + fakeUser, + (err, swimlaneId) => { + // Insert the reference to out board templates swimlane + Users.update(fakeUserId.get(), { + $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId }, + }); + } + ); + } + ); }); }); } 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). @@ -831,7 +905,7 @@ if (Meteor.isServer) { // 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: ''}}); + Users.update(doc._id, { $set: { createdThroughApi: '' } }); return; } @@ -840,7 +914,10 @@ if (Meteor.isServer) { // If ldap, bypass the inviation code if the self registration isn't allowed. // TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type if (doc.authenticationMethod !== 'ldap' && 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 { @@ -852,8 +929,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 } }); } } }); @@ -862,13 +939,12 @@ if (Meteor.isServer) { // USERS REST API if (Meteor.isServer) { // Middleware which checks that API is enabled. - JsonRoutes.Middleware.use(function (req, res, next) { + JsonRoutes.Middleware.use(function(req, res, next) { const api = req.url.search('api'); - if (api === 1 && process.env.WITH_API === 'true' || api === -1){ + if ((api === 1 && process.env.WITH_API === 'true') || api === -1) { return next(); - } - else { - res.writeHead(301, {Location: '/'}); + } else { + res.writeHead(301, { Location: '/' }); return res.end(); } }); @@ -882,14 +958,13 @@ if (Meteor.isServer) { JsonRoutes.add('GET', '/api/user', function(req, res) { try { Authentication.checkLoggedIn(req.userId); - const data = Meteor.users.findOne({ _id: req.userId}); + const data = Meteor.users.findOne({ _id: req.userId }); delete data.services; JsonRoutes.sendResult(res, { code: 200, data, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -906,17 +981,16 @@ if (Meteor.isServer) { * @return_type [{ _id: string, * username: string}] */ - JsonRoutes.add('GET', '/api/users', function (req, res) { + 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) { + data: Meteor.users.find({}).map(function(doc) { return { _id: doc._id, username: doc.username }; }), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -934,7 +1008,7 @@ if (Meteor.isServer) { * @param {string} userId the user ID * @return_type Users */ - JsonRoutes.add('GET', '/api/users/:userId', function (req, res) { + JsonRoutes.add('GET', '/api/users/:userId', function(req, res) { try { Authentication.checkUserId(req.userId); const id = req.params.userId; @@ -942,8 +1016,7 @@ if (Meteor.isServer) { code: 200, data: Meteor.users.findOne({ _id: id }), }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -968,7 +1041,7 @@ if (Meteor.isServer) { * @return_type {_id: string, * title: string} */ - JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) { + JsonRoutes.add('PUT', '/api/users/:userId', function(req, res) { try { Authentication.checkUserId(req.userId); const id = req.params.userId; @@ -990,8 +1063,16 @@ if (Meteor.isServer) { }; }); } else { - if ((action === 'disableLogin') && (id !== req.userId)) { - Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } }); + 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: '' } }); } @@ -1002,8 +1083,7 @@ if (Meteor.isServer) { code: 200, data, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1030,13 +1110,16 @@ if (Meteor.isServer) { * @return_type {_id: string, * title: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function (req, res) { + JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function( + req, + res + ) { try { Authentication.checkUserId(req.userId); const userId = req.params.userId; const boardId = req.params.boardId; const action = req.body.action; - const {isAdmin, isNoComments, isCommentOnly} = req.body; + const { isAdmin, isNoComments, isCommentOnly } = req.body; let data = Meteor.users.findOne({ _id: userId }); if (data !== undefined) { if (action === 'add') { @@ -1045,10 +1128,16 @@ if (Meteor.isServer) { }).map(function(board) { if (!board.hasMember(userId)) { board.addMember(userId); - function isTrue(data){ + function isTrue(data) { return data.toLowerCase() === 'true'; } - board.setMemberPermission(userId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), userId); + board.setMemberPermission( + userId, + isTrue(isAdmin), + isTrue(isNoComments), + isTrue(isCommentOnly), + userId + ); } return { _id: board._id, @@ -1061,8 +1150,7 @@ if (Meteor.isServer) { code: 200, data: query, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1084,40 +1172,43 @@ if (Meteor.isServer) { * @return_type {_id: string, * title: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/remove', function (req, res) { - try { - Authentication.checkUserId(req.userId); - const userId = req.params.userId; - const boardId = req.params.boardId; - const action = req.body.action; - let data = Meteor.users.findOne({ _id: userId }); - if (data !== undefined) { - if (action === 'remove') { - data = Boards.find({ - _id: boardId, - }).map(function(board) { - if (board.hasMember(userId)) { - board.removeMember(userId); - } - return { - _id: board._id, - title: board.title, - }; - }); + JsonRoutes.add( + 'POST', + '/api/boards/:boardId/members/:userId/remove', + function(req, res) { + try { + Authentication.checkUserId(req.userId); + const userId = req.params.userId; + const boardId = req.params.boardId; + const action = req.body.action; + let data = Meteor.users.findOne({ _id: userId }); + if (data !== undefined) { + if (action === 'remove') { + data = Boards.find({ + _id: boardId, + }).map(function(board) { + if (board.hasMember(userId)) { + board.removeMember(userId); + } + return { + _id: board._id, + title: board.title, + }; + }); + } } + JsonRoutes.sendResult(res, { + code: 200, + data: query, + }); + } catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); } - JsonRoutes.sendResult(res, { - code: 200, - data: query, - }); } - catch (error) { - JsonRoutes.sendResult(res, { - code: 200, - data: error, - }); - } - }); + ); /** * @operation new_user @@ -1131,7 +1222,7 @@ if (Meteor.isServer) { * @param {string} password the password of the new user * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/users/', function (req, res) { + JsonRoutes.add('POST', '/api/users/', function(req, res) { try { Authentication.checkUserId(req.userId); const id = Accounts.createUser({ @@ -1146,8 +1237,7 @@ if (Meteor.isServer) { _id: id, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1165,7 +1255,7 @@ if (Meteor.isServer) { * @param {string} userId the ID of the user to delete * @return_type {_id: string} */ - JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) { + JsonRoutes.add('DELETE', '/api/users/:userId', function(req, res) { try { Authentication.checkUserId(req.userId); const id = req.params.userId; @@ -1176,8 +1266,7 @@ if (Meteor.isServer) { _id: id, }, }); - } - catch (error) { + } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, @@ -1185,3 +1274,5 @@ if (Meteor.isServer) { } }); } + +export default Users; -- cgit v1.2.3-1-g7c22