diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/accountSettings.js | 12 | ||||
-rw-r--r-- | models/activities.js | 14 | ||||
-rw-r--r-- | models/attachments.js | 12 | ||||
-rw-r--r-- | models/boards.js | 313 | ||||
-rw-r--r-- | models/cards.js | 134 | ||||
-rw-r--r-- | models/checklists.js | 2 | ||||
-rw-r--r-- | models/export.js | 18 | ||||
-rw-r--r-- | models/lists.js | 6 | ||||
-rw-r--r-- | models/settings.js | 20 | ||||
-rw-r--r-- | models/trelloCreator.js | 24 | ||||
-rw-r--r-- | models/users.js | 286 | ||||
-rw-r--r-- | models/wekanCreator.js | 2 |
12 files changed, 750 insertions, 93 deletions
diff --git a/models/accountSettings.js b/models/accountSettings.js index f61614b8..a20303f5 100644 --- a/models/accountSettings.js +++ b/models/accountSettings.js @@ -82,4 +82,16 @@ if (Meteor.isServer) { }); } +AccountSettings.helpers({ + allowEmailChange() { + return AccountSettings.findOne('accounts-allowEmailChange').booleanValue; + }, + allowUserNameChange() { + return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue; + }, + allowUserDelete() { + return AccountSettings.findOne('accounts-allowUserDelete').booleanValue; + }, +}); + export default AccountSettings; diff --git a/models/activities.js b/models/activities.js index 19e3fb7d..b5fcb7d8 100644 --- a/models/activities.js +++ b/models/activities.js @@ -108,7 +108,7 @@ if (Meteor.isServer) { let participants = []; let watchers = []; let title = 'act-activity-notify'; - let board = null; + const board = Boards.findOne(activity.boardId); const description = `act-${activity.activityType}`; const params = { activityId: activity._id, @@ -122,8 +122,11 @@ if (Meteor.isServer) { params.userId = activity.userId; } if (activity.boardId) { - board = activity.board(); - params.board = board.title; + if (board.title.length > 0) { + params.board = board.title; + } else { + params.board = ''; + } title = 'act-withBoardTitle'; params.url = board.absoluteUrl(); params.boardId = activity.boardId; @@ -279,7 +282,10 @@ if (Meteor.isServer) { ); } Notifications.getUsers(watchers).forEach(user => { - Notifications.notify(user, title, description, params); + // don't notify a user of their own behavior + if (user._id !== userId) { + Notifications.notify(user, title, description, params); + } }); const integrations = Integrations.find({ diff --git a/models/attachments.js b/models/attachments.js index 9b8ec04f..3fe1d745 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -219,6 +219,9 @@ if (Meteor.isServer) { type: 'card', activityType: 'addAttachment', attachmentId: doc._id, + // this preserves the name so that notifications can be meaningful after + // this file is removed + attachmentName: doc.original.name, boardId: doc.boardId, cardId: doc.cardId, listId: doc.listId, @@ -246,18 +249,15 @@ if (Meteor.isServer) { type: 'card', activityType: 'deleteAttachment', attachmentId: doc._id, + // this preserves the name so that notifications can be meaningful after + // this file is removed + attachmentName: doc.original.name, boardId: doc.boardId, cardId: doc.cardId, listId: doc.listId, swimlaneId: doc.swimlaneId, }); }); - - Attachments.files.after.remove((userId, doc) => { - Activities.remove({ - attachmentId: doc._id, - }); - }); } export default Attachments; diff --git a/models/boards.js b/models/boards.js index 857aa963..26dc6127 100644 --- a/models/boards.js +++ b/models/boards.js @@ -185,6 +185,7 @@ Boards.attachSchema( isActive: true, isNoComments: false, isCommentOnly: false, + isWorker: false, }, ]; } @@ -222,6 +223,13 @@ Boards.attachSchema( type: Boolean, optional: true, }, + 'members.$.isWorker': { + /** + * Is the member only allowed to move card, assign himself to card and comment + */ + type: Boolean, + optional: true, + }, permission: { /** * visibility of the board @@ -270,6 +278,7 @@ Boards.attachSchema( optional: true, defaultValue: null, }, + subtasksDefaultListId: { /** * The default List ID assigned to subtasks. @@ -278,6 +287,19 @@ Boards.attachSchema( optional: true, defaultValue: null, }, + + dateSettingsDefaultBoardId: { + type: String, + optional: true, + defaultValue: null, + }, + + dateSettingsDefaultListId: { + type: String, + optional: true, + defaultValue: null, + }, + allowsSubtasks: { /** * Does the board allows subtasks? @@ -285,6 +307,127 @@ Boards.attachSchema( type: Boolean, defaultValue: true, }, + + allowsAttachments: { + /** + * Does the board allows attachments? + */ + type: Boolean, + defaultValue: true, + }, + + allowsChecklists: { + /** + * Does the board allows checklists? + */ + type: Boolean, + defaultValue: true, + }, + + allowsComments: { + /** + * Does the board allows comments? + */ + type: Boolean, + defaultValue: true, + }, + + allowsDescriptionTitle: { + /** + * Does the board allows description title? + */ + type: Boolean, + defaultValue: true, + }, + + allowsDescriptionText: { + /** + * Does the board allows description text? + */ + type: Boolean, + defaultValue: true, + }, + + allowsActivities: { + /** + * Does the board allows comments? + */ + type: Boolean, + defaultValue: true, + }, + + allowsLabels: { + /** + * Does the board allows labels? + */ + type: Boolean, + defaultValue: true, + }, + + allowsAssignee: { + /** + * Does the board allows assignee? + */ + type: Boolean, + defaultValue: true, + }, + + allowsMembers: { + /** + * Does the board allows members? + */ + type: Boolean, + defaultValue: true, + }, + + allowsRequestedBy: { + /** + * Does the board allows requested by? + */ + type: Boolean, + defaultValue: true, + }, + + allowsAssignedBy: { + /** + * Does the board allows requested by? + */ + type: Boolean, + defaultValue: true, + }, + + allowsReceivedDate: { + /** + * Does the board allows received date? + */ + type: Boolean, + defaultValue: true, + }, + + allowsStartDate: { + /** + * Does the board allows start date? + */ + type: Boolean, + defaultValue: true, + }, + + allowsEndDate: { + /** + * Does the board allows end date? + */ + type: Boolean, + defaultValue: true, + }, + + allowsDueDate: { + /** + * Does the board allows due date? + */ + type: Boolean, + defaultValue: true, + }, + presentParentTask: { /** * Controls how to present the parent task: @@ -350,6 +493,14 @@ Boards.attachSchema( type: String, defaultValue: 'board', }, + sort: { + /** + * Sort value + */ + type: Number, + decimal: true, + defaultValue: -1, + }, }), ); @@ -538,6 +689,7 @@ Boards.helpers({ isActive: true, isAdmin: false, isNoComments: true, + isWorker: false, }); }, @@ -547,6 +699,17 @@ Boards.helpers({ isActive: true, isAdmin: false, isCommentOnly: true, + isWorker: false, + }); + }, + + hasWorker(memberId) { + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: false, + isCommentOnly: false, + isWorker: true, }); }, @@ -651,7 +814,11 @@ Boards.helpers({ if (term) { const regex = new RegExp(term, 'i'); - query.$or = [{ title: regex }, { description: regex }]; + query.$or = [ + { title: regex }, + { description: regex }, + { customFields: { $elemMatch: { value: regex } } }, + ]; } return Cards.find(query, projection); @@ -690,6 +857,39 @@ Boards.helpers({ return Boards.findOne(this.getDefaultSubtasksBoardId()); }, + //Date Settings option such as received date, start date and so on. + getDefaultDateSettingsBoardId() { + if ( + this.dateSettingsDefaultBoardId === null || + this.dateSettingsDefaultBoardId === undefined + ) { + this.dateSettingsDefaultBoardId = Boards.insert({ + title: `^${this.title}^`, + permission: this.permission, + members: this.members, + color: this.color, + description: TAPi18n.__('default-dates-board', { + board: this.title, + }), + }); + + Swimlanes.insert({ + title: TAPi18n.__('default'), + boardId: this.dateSettingsDefaultBoardId, + }); + Boards.update(this._id, { + $set: { + dateSettingsDefaultBoardId: this.dateSettingsDefaultBoardId, + }, + }); + } + return this.dateSettingsDefaultBoardId; + }, + + getDefaultDateSettingsBoard() { + return Boards.findOne(this.getDefaultDateSettingsBoardId()); + }, + getDefaultSubtasksListId() { if ( this.subtasksDefaultListId === null || @@ -708,6 +908,24 @@ Boards.helpers({ return Lists.findOne(this.getDefaultSubtasksListId()); }, + getDefaultDateSettingsListId() { + if ( + this.dateSettingsDefaultListId === null || + this.dateSettingsDefaultListId === undefined + ) { + this.dateSettingsDefaultListId = Lists.insert({ + title: TAPi18n.__('queue'), + boardId: this._id, + }); + this.setDateSettingsDefaultListId(this.dateSettingsDefaultListId); + } + return this.dateSettingsDefaultListId; + }, + + getDefaultDateSettingsList() { + return Lists.findOne(this.getDefaultDateSettingsListId()); + }, + getDefaultSwimline() { let result = Swimlanes.findOne({ boardId: this._id }); if (result === undefined) { @@ -849,6 +1067,7 @@ Boards.mutations({ isActive: true, isNoComments: false, isCommentOnly: false, + isWorker: false, }, }, }; @@ -881,6 +1100,7 @@ Boards.mutations({ isAdmin, isNoComments, isCommentOnly, + isWorker, currentUserId = Meteor.userId(), ) { const memberIndex = this.memberIndex(memberId); @@ -894,6 +1114,7 @@ Boards.mutations({ [`members.${memberIndex}.isAdmin`]: isAdmin, [`members.${memberIndex}.isNoComments`]: isNoComments, [`members.${memberIndex}.isCommentOnly`]: isCommentOnly, + [`members.${memberIndex}.isWorker`]: isWorker, }, }; }, @@ -902,6 +1123,66 @@ Boards.mutations({ return { $set: { allowsSubtasks } }; }, + setAllowsMembers(allowsMembers) { + return { $set: { allowsMembers } }; + }, + + setAllowsChecklists(allowsChecklists) { + return { $set: { allowsChecklists } }; + }, + + setAllowsAssignee(allowsAssignee) { + return { $set: { allowsAssignee } }; + }, + + setAllowsAssignedBy(allowsAssignedBy) { + return { $set: { allowsAssignedBy } }; + }, + + setAllowsRequestedBy(allowsRequestedBy) { + return { $set: { allowsRequestedBy } }; + }, + + setAllowsAttachments(allowsAttachments) { + return { $set: { allowsAttachments } }; + }, + + setAllowsLabels(allowsLabels) { + return { $set: { allowsLabels } }; + }, + + setAllowsComments(allowsComments) { + return { $set: { allowsComments } }; + }, + + setAllowsDescriptionTitle(allowsDescriptionTitle) { + return { $set: { allowsDescriptionTitle } }; + }, + + setAllowsDescriptionText(allowsDescriptionText) { + return { $set: { allowsDescriptionText } }; + }, + + setAllowsActivities(allowsActivities) { + return { $set: { allowsActivities } }; + }, + + setAllowsReceivedDate(allowsReceivedDate) { + return { $set: { allowsReceivedDate } }; + }, + + setAllowsStartDate(allowsStartDate) { + return { $set: { allowsStartDate } }; + }, + + setAllowsEndDate(allowsEndDate) { + return { $set: { allowsEndDate } }; + }, + + setAllowsDueDate(allowsDueDate) { + return { $set: { allowsDueDate } }; + }, + setSubtasksDefaultBoardId(subtasksDefaultBoardId) { return { $set: { subtasksDefaultBoardId } }; }, @@ -913,6 +1194,10 @@ Boards.mutations({ setPresentParentTask(presentParentTask) { return { $set: { presentParentTask } }; }, + + move(sortIndex) { + return { $set: { sort: sortIndex } }; + }, }); function boardRemover(userId, doc) { @@ -1010,6 +1295,17 @@ if (Meteor.isServer) { }); } +// Insert new board at last position in sort order. +Boards.before.insert((userId, doc) => { + const lastBoard = Boards.findOne( + { sort: { $exists: true } }, + { sort: { sort: -1 } }, + ); + if (lastBoard && typeof lastBoard.sort !== 'undefined') { + doc.sort = lastBoard.sort + 1; + } +}); + if (Meteor.isServer) { // Let MongoDB ensure that a member is not included twice in the same board Meteor.startup(() => { @@ -1193,7 +1489,7 @@ if (Meteor.isServer) { 'members.userId': paramUserId, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ).map(function(board) { return { @@ -1223,7 +1519,12 @@ if (Meteor.isServer) { Authentication.checkUserId(req.userId); JsonRoutes.sendResult(res, { code: 200, - data: Boards.find({ permission: 'public' }).map(function(doc) { + data: Boards.find( + { permission: 'public' }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ).map(function(doc) { return { _id: doc._id, title: doc.title, @@ -1281,6 +1582,7 @@ if (Meteor.isServer) { * @param {boolean} [isActive] is the board active (default true) * @param {boolean} [isNoComments] disable comments (default false) * @param {boolean} [isCommentOnly] only enable comments (default false) + * @param {boolean} [isWorker] only move cards, assign himself to card and comment (default false) * @param {string} [permission] "private" board <== Set to "public" if you * want public Wekan board * @param {string} [color] the color of the board @@ -1300,6 +1602,7 @@ if (Meteor.isServer) { isActive: req.body.isActive || true, isNoComments: req.body.isNoComments || false, isCommentOnly: req.body.isCommentOnly || false, + isWorker: req.body.isWorker || false, }, ], permission: req.body.permission || 'private', @@ -1403,6 +1706,7 @@ if (Meteor.isServer) { * @param {boolean} isAdmin admin capability * @param {boolean} isNoComments NoComments capability * @param {boolean} isCommentOnly CommentsOnly capability + * @param {boolean} isWorker Worker capability */ JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function( req, @@ -1411,7 +1715,7 @@ if (Meteor.isServer) { try { const boardId = req.params.boardId; const memberId = req.params.memberId; - const { isAdmin, isNoComments, isCommentOnly } = req.body; + const { isAdmin, isNoComments, isCommentOnly, isWorker } = req.body; Authentication.checkBoardAccess(req.userId, boardId); const board = Boards.findOne({ _id: boardId }); function isTrue(data) { @@ -1426,6 +1730,7 @@ if (Meteor.isServer) { isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), + isTrue(isWorker), req.userId, ); diff --git a/models/cards.js b/models/cards.js index 496c69b3..4197f7ab 100644 --- a/models/cards.js +++ b/models/cards.js @@ -304,6 +304,42 @@ Cards.attachSchema( optional: true, defaultValue: '', }, + vote: { + /** + * vote object, see below + */ + type: Object, + optional: true, + }, + 'vote.question': { + type: String, + defaultValue: '', + }, + 'vote.positive': { + /** + * list of members (user IDs) + */ + type: [String], + optional: true, + defaultValue: [], + }, + 'vote.negative': { + /** + * list of members (user IDs) + */ + type: [String], + optional: true, + defaultValue: [], + }, + 'vote.end': { + type: Date, + optional: true, + defaultValue: null, + }, + 'vote.public': { + type: Boolean, + defaultValue: false, + }, }), ); @@ -980,6 +1016,50 @@ Cards.helpers({ } }, + getVoteQuestion() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + if (card && card.vote) return card.vote.question; + else return null; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({ _id: this.linkedId }); + if (board && board.vote) return board.vote.question; + else return null; + } else if (this.vote) { + return this.vote.question; + } else { + return null; + } + }, + + getVotePublic() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + if (card && card.vote) return card.vote.public; + else return null; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({ _id: this.linkedId }); + if (board && board.vote) return board.vote.public; + else return null; + } else if (this.vote) { + return this.vote.public; + } else { + return null; + } + }, + + voteMemberPositive() { + if (this.vote && this.vote.positive) + return Users.find({ _id: { $in: this.vote.positive } }); + return []; + }, + + voteMemberNegative() { + if (this.vote && this.vote.negative) + return Users.find({ _id: { $in: this.vote.negative } }); + return []; + }, + getId() { if (this.isLinked()) { return this.linkedId; @@ -1396,6 +1476,58 @@ Cards.mutations({ }, }; }, + setVoteQuestion(question, publicVote) { + return { + $set: { + vote: { + question, + public: publicVote, + positive: [], + negative: [], + }, + }, + }; + }, + unsetVote() { + return { + $unset: { + vote: '', + }, + }; + }, + setVote(userId, forIt) { + switch (forIt) { + case true: + // vote for it + return { + $pull: { + 'vote.negative': userId, + }, + $addToSet: { + 'vote.positive': userId, + }, + }; + case false: + // vote against + return { + $pull: { + 'vote.positive': userId, + }, + $addToSet: { + 'vote.negative': userId, + }, + }; + + default: + // Remove votes + return { + $pull: { + 'vote.positive': userId, + 'vote.negative': userId, + }, + }; + } + }, }); //FUNCTIONS FOR creation of Activities @@ -2008,7 +2140,7 @@ if (Meteor.isServer) { const paramBoardId = req.params.boardId; // Check user has permission to add card to the board const board = Boards.findOne({ - _id: paramBoardId + _id: paramBoardId, }); const addPermission = allowIsBoardMemberCommentOnly(req.userId, board); Authentication.checkAdminOrCondition(req.userId, addPermission); diff --git a/models/checklists.js b/models/checklists.js index 11aba71b..cf73e500 100644 --- a/models/checklists.js +++ b/models/checklists.js @@ -288,7 +288,7 @@ if (Meteor.isServer) { const paramBoardId = req.params.boardId; // Check user has permission to add checklist to the card const board = Boards.findOne({ - _id: paramBoardId + _id: paramBoardId, }); const addPermission = allowIsBoardMemberCommentOnly(req.userId, board); Authentication.checkAdminOrCondition(req.userId, addPermission); diff --git a/models/export.js b/models/export.js index cc979ce0..339123c8 100644 --- a/models/export.js +++ b/models/export.js @@ -24,7 +24,6 @@ if (Meteor.isServer) { JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) { const boardId = req.params.boardId; let user = null; - const loginToken = req.query.authToken; if (loginToken) { const hashToken = Accounts._hashLoginToken(loginToken); @@ -35,7 +34,6 @@ if (Meteor.isServer) { Authentication.checkUserId(req.userId); user = Users.findOne({ _id: req.userId, isAdmin: true }); } - const exporter = new Exporter(boardId); if (exporter.canExport(user)) { JsonRoutes.sendResult(res, { @@ -137,8 +135,11 @@ export class Exporter { // [Old] for attachments we only export IDs and absolute url to original doc // [New] Encode attachment to base64 + const getBase64Data = function(doc, callback) { - let buffer = new Buffer(0); + let buffer = Buffer.allocUnsafe(0); + buffer.fill(0); + // callback has the form function (err, res) {} const tmpFile = path.join( os.tmpdir(), @@ -149,14 +150,16 @@ export class Exporter { readStream.on('data', function(chunk) { buffer = Buffer.concat([buffer, chunk]); }); + readStream.on('error', function(err) { - callback(err, null); + callback(null, null); }); readStream.on('end', function() { // done fs.unlink(tmpFile, () => { //ignored }); + callback(null, buffer.toString('base64')); }); readStream.pipe(tmpWriteable); @@ -165,11 +168,14 @@ export class Exporter { result.attachments = Attachments.find(byBoard) .fetch() .map(attachment => { + let filebase64 = null; + filebase64 = getBase64DataSync(attachment); + return { _id: attachment._id, cardId: attachment.cardId, - // url: FlowRouter.url(attachment.url()), - file: getBase64DataSync(attachment), + //url: FlowRouter.url(attachment.url()), + file: filebase64, name: attachment.original.name, type: attachment.original.type, }; diff --git a/models/lists.js b/models/lists.js index f06b15b1..b123ab4f 100644 --- a/models/lists.js +++ b/models/lists.js @@ -369,6 +369,9 @@ if (Meteor.isServer) { activityType: 'createList', boardId: doc.boardId, listId: doc._id, + // this preserves the name so that the activity can be useful after the + // list is deleted + title: doc.title, }); }); @@ -397,6 +400,9 @@ if (Meteor.isServer) { activityType: 'archivedList', listId: doc._id, boardId: doc.boardId, + // this preserves the name so that the activity can be useful after the + // list is deleted + title: doc.title, }); } }); diff --git a/models/settings.js b/models/settings.js index 8eb02c5b..fb823205 100644 --- a/models/settings.js +++ b/models/settings.js @@ -33,14 +33,6 @@ Settings.attachSchema( type: String, optional: true, }, - customHTMLafterBodyStart: { - type: String, - optional: true, - }, - customHTMLbeforeBodyEnd: { - type: String, - optional: true, - }, displayAuthenticationMethod: { type: Boolean, optional: true, @@ -206,6 +198,10 @@ if (Meteor.isServer) { return process.env.CAS_ENABLED === 'true'; } + function isApiEnabled() { + return process.env.WITH_API === 'true'; + } + Meteor.methods({ sendInvitation(emails, boards) { check(emails, [String]); @@ -322,6 +318,10 @@ if (Meteor.isServer) { return isCasEnabled(); }, + _isApiEnabled() { + return isApiEnabled(); + }, + // Gets all connection methods to use it in the Template getAuthenticationsEnabled() { return { @@ -334,6 +334,10 @@ if (Meteor.isServer) { getDefaultAuthenticationMethod() { return process.env.DEFAULT_AUTHENTICATION_METHOD; }, + + isPasswordLoginDisabled() { + return process.env.PASSWORD_LOGIN_ENABLED === 'false'; + }, }); } diff --git a/models/trelloCreator.js b/models/trelloCreator.js index cb1a6a67..1c5bcd93 100644 --- a/models/trelloCreator.js +++ b/models/trelloCreator.js @@ -285,6 +285,30 @@ export class TrelloCreator { cardToCreate.members = wekanMembers; } } + // add vote + if (card.idMembersVoted) { + // Trello only know's positive votes + const positiveVotes = []; + card.idMembersVoted.forEach(trelloId => { + if (this.members[trelloId]) { + const wekanId = this.members[trelloId]; + // we may map multiple Trello members to the same wekan user + // in which case we risk adding the same user multiple times + if (!positiveVotes.find(wId => wId === wekanId)) { + positiveVotes.push(wekanId); + } + } + return true; + }); + if (positiveVotes.length > 0) { + cardToCreate.vote = { + question: cardToCreate.title, + public: true, + positive: positiveVotes, + }; + } + } + // insert card const cardId = Cards.direct.insert(cardToCreate); // keep track of Trello id => Wekan id diff --git a/models/users.js b/models/users.js index 83a224ba..a1bc5b0f 100644 --- a/models/users.js +++ b/models/users.js @@ -1,3 +1,5 @@ +import { SyncedCron } from 'meteor/percolate:synced-cron'; + // Sandstorm context is detected using the METEOR_SETTINGS environment variable // in the package definition. const isSandstorm = @@ -165,7 +167,20 @@ Users.attachSchema( /** * enabled notifications for the user */ - type: [String], + type: [Object], + optional: true, + }, + 'profile.notifications.$.activity': { + /** + * The id of the activity this notification references + */ + type: String, + }, + 'profile.notifications.$.read': { + /** + * the date on which this notification was read + */ + type: Date, optional: true, }, 'profile.showCardsCountAt': { @@ -175,6 +190,13 @@ Users.attachSchema( type: Number, optional: true, }, + 'profile.startDayOfWeek': { + /** + * startDayOfWeek field of the user + */ + type: Number, + optional: true, + }, 'profile.starredBoards': { /** * list of starred board IDs @@ -352,8 +374,18 @@ if (Meteor.isClient) { return board && board.hasCommentOnly(this._id); }, - isBoardAdmin() { + isNotWorker() { + const board = Boards.findOne(Session.get('currentBoard')); + return board && board.hasMember(this._id) && !board.hasWorker(this._id); + }, + + isWorker() { const board = Boards.findOne(Session.get('currentBoard')); + return board && board.hasWorker(this._id); + }, + + isBoardAdmin(boardId = Session.get('currentBoard')) { + const board = Boards.findOne(boardId); return board && board.hasAdmin(this._id); }, }); @@ -361,12 +393,20 @@ if (Meteor.isClient) { Users.helpers({ boards() { - return Boards.find({ 'members.userId': this._id }); + return Boards.find( + { 'members.userId': this._id }, + { sort: { sort: 1 /* boards default sorting */ } }, + ); }, starredBoards() { const { starredBoards = [] } = this.profile || {}; - return Boards.find({ archived: false, _id: { $in: starredBoards } }); + return Boards.find( + { archived: false, _id: { $in: starredBoards } }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ); }, hasStarred(boardId) { @@ -376,7 +416,12 @@ Users.helpers({ invitedBoards() { const { invitedBoards = [] } = this.profile || {}; - return Boards.find({ archived: false, _id: { $in: invitedBoards } }); + return Boards.find( + { archived: false, _id: { $in: invitedBoards } }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ); }, isInvitedTo(boardId) { @@ -419,6 +464,20 @@ Users.helpers({ return _.contains(notifications, activityId); }, + notifications() { + const { notifications = [] } = this.profile || {}; + for (const index in notifications) { + if (!notifications.hasOwnProperty(index)) continue; + const notification = notifications[index]; + // this preserves their db sort order for editing + notification.dbIndex = index; + notification.activity = Activities.findOne(notification.activity); + } + // this sorts them newest to oldest to match Trello's behavior + notifications.reverse(); + return notifications; + }, + hasShowDesktopDragHandles() { const profile = this.profile || {}; return profile.showDesktopDragHandles || false; @@ -469,6 +528,15 @@ Users.helpers({ return profile.language || 'en'; }, + getStartDayOfWeek() { + const profile = this.profile || {}; + if (typeof profile.startDayOfWeek === 'undefined') { + // default is 'Monday' (1) + return 1; + } + return profile.startDayOfWeek; + }, + getTemplatesBoardId() { return (this.profile || {}).templatesBoardId; }, @@ -563,7 +631,7 @@ Users.mutations({ addNotification(activityId) { return { $addToSet: { - 'profile.notifications': activityId, + 'profile.notifications': { activity: activityId }, }, }; }, @@ -571,7 +639,7 @@ Users.mutations({ removeNotification(activityId) { return { $pull: { - 'profile.notifications': activityId, + 'profile.notifications': { activity: activityId }, }, }; }, @@ -600,6 +668,10 @@ Users.mutations({ return { $set: { 'profile.showCardsCountAt': limit } }; }, + setStartDayOfWeek(startDay) { + return { $set: { 'profile.startDayOfWeek': startDay } }; + }, + setBoardView(view) { return { $set: { @@ -610,16 +682,6 @@ Users.mutations({ }); Meteor.methods({ - setUsername(username, userId) { - check(username, String); - check(userId, String); - const nUsersWithUsername = Users.find({ username }).count(); - if (nUsersWithUsername > 0) { - throw new Meteor.Error('username-already-taken'); - } else { - Users.update(userId, { $set: { username } }); - } - }, setListSortBy(value) { check(value, String); Meteor.user().setListSortBy(value); @@ -640,51 +702,101 @@ Meteor.methods({ check(limit, Number); Meteor.user().setShowCardsCountAt(limit); }, - setEmail(email, userId) { - if (Array.isArray(email)) { - email = email.shift(); - } - check(email, String); - 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, - }, - ], - }, - }); - } - }, - setUsernameAndEmail(username, email, userId) { - check(username, String); - if (Array.isArray(email)) { - email = email.shift(); - } - check(email, String); - check(userId, String); - Meteor.call('setUsername', username, userId); - Meteor.call('setEmail', email, userId); - }, - setPassword(newPassword, userId) { - check(userId, String); - check(newPassword, String); - if (Meteor.user().isAdmin) { - Accounts.setPassword(userId, newPassword); - } + changeStartDayOfWeek(startDay) { + check(startDay, Number); + Meteor.user().setStartDayOfWeek(startDay); }, }); if (Meteor.isServer) { Meteor.methods({ + setCreateUser(fullname, username, password, isAdmin, isActive, email) { + if (Meteor.user() && Meteor.user().isAdmin) { + check(fullname, String); + check(username, String); + check(password, String); + check(isAdmin, String); + check(isActive, String); + check(email, String); + + const nUsersWithUsername = Users.find({ username }).count(); + const nUsersWithEmail = Users.find({ email }).count(); + if (nUsersWithUsername > 0) { + throw new Meteor.Error('username-already-taken'); + } else if (nUsersWithEmail > 0) { + throw new Meteor.Error('email-already-taken'); + } else { + Accounts.createUser({ + fullname, + username, + password, + isAdmin, + isActive, + email: email.toLowerCase(), + from: 'admin', + }); + } + } + }, + setUsername(username, userId) { + if (Meteor.user() && Meteor.user().isAdmin) { + check(username, String); + check(userId, String); + const nUsersWithUsername = Users.find({ username }).count(); + if (nUsersWithUsername > 0) { + throw new Meteor.Error('username-already-taken'); + } else { + Users.update(userId, { $set: { username } }); + } + } + }, + setEmail(email, userId) { + if (Meteor.user() && Meteor.user().isAdmin) { + if (Array.isArray(email)) { + email = email.shift(); + } + check(email, String); + 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, + }, + ], + }, + }); + } + } + }, + setUsernameAndEmail(username, email, userId) { + if (Meteor.user() && Meteor.user().isAdmin) { + check(username, String); + if (Array.isArray(email)) { + email = email.shift(); + } + check(email, String); + check(userId, String); + Meteor.call('setUsername', username, userId); + Meteor.call('setEmail', email, userId); + } + }, + setPassword(newPassword, userId) { + if (Meteor.user() && Meteor.user().isAdmin) { + check(userId, String); + check(newPassword, String); + if (Meteor.user().isAdmin) { + Accounts.setPassword(userId, newPassword); + } + } + }, // we accept userId, username, email inviteUserToBoard(username, boardId) { check(username, String); @@ -716,8 +828,9 @@ if (Meteor.isServer) { throw new Meteor.Error('error-user-notAllowSelf'); } else { if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist'); - if (Settings.findOne().disableRegistration) + if (Settings.findOne({ disableRegistration: true })) { throw new Meteor.Error('error-user-notCreated'); + } // Set in lowercase email before creating account const email = username.toLowerCase(); username = email.substring(0, posAt); @@ -738,6 +851,16 @@ if (Meteor.isServer) { board.addMember(user._id); user.addInvite(boardId); + //Check if there is a subtasks board + if (board.subtasksDefaultBoardId) { + const subBoard = Boards.findOne(board.subtasksDefaultBoardId); + //If there is, also add user to that board + if (subBoard) { + subBoard.addMember(user._id); + user.addInvite(subBoard._id); + } + } + try { const params = { user: user.username, @@ -852,6 +975,39 @@ if (Meteor.isServer) { }); } +const addCronJob = _.debounce( + Meteor.bindEnvironment(function notificationCleanupDebounced() { + // passed in the removeAge has to be a number standing for the number of days after a notification is read before we remove it + const envRemoveAge = + process.env.NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE; + // default notifications will be removed 2 days after they are read + const defaultRemoveAge = 2; + const removeAge = parseInt(envRemoveAge, 10) || defaultRemoveAge; + + SyncedCron.add({ + name: 'notification_cleanup', + schedule: parser => parser.text('every 1 days'), + job: () => { + for (const user of Users.find()) { + if (!user.profile || !user.profile.notifications) continue; + for (const notification of user.profile.notifications) { + if (notification.read) { + const removeDate = new Date(notification.read); + removeDate.setDate(removeDate.getDate() + removeAge); + if (removeDate <= new Date()) { + user.removeNotification(notification.activity); + } + } + } + } + }, + }); + + SyncedCron.start(); + }), + 500, +); + if (Meteor.isServer) { // Let mongoDB ensure username unicity Meteor.startup(() => { @@ -865,6 +1021,9 @@ if (Meteor.isServer) { }, { unique: true }, ); + Meteor.defer(() => { + addCronJob(); + }); }); // OLD WAY THIS CODE DID WORK: When user is last admin of board, @@ -1170,10 +1329,13 @@ if (Meteor.isServer) { let data = Meteor.users.findOne({ _id: id }); if (data !== undefined) { if (action === 'takeOwnership') { - data = Boards.find({ - 'members.userId': id, - 'members.isAdmin': true, - }).map(function(board) { + data = Boards.find( + { + 'members.userId': id, + 'members.isAdmin': true, + }, + { sort: { sort: 1 /* boards default sorting */ } }, + ).map(function(board) { if (board.hasMember(req.userId)) { board.removeMember(req.userId); } diff --git a/models/wekanCreator.js b/models/wekanCreator.js index ec85d93f..9914f817 100644 --- a/models/wekanCreator.js +++ b/models/wekanCreator.js @@ -441,7 +441,7 @@ export class WekanCreator { }); } else if (att.file) { file.attachData( - new Buffer(att.file, 'base64'), + Buffer.from(att.file, 'base64'), { type: att.type, }, |