summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
Diffstat (limited to 'models')
-rw-r--r--models/accountSettings.js8
-rw-r--r--models/activities.js11
-rw-r--r--models/attachments.js158
-rw-r--r--models/boards.js214
-rw-r--r--models/cards.js714
-rw-r--r--models/checklistItems.js50
-rw-r--r--models/checklists.js116
-rw-r--r--models/customFields.js132
-rw-r--r--models/export.js8
-rw-r--r--models/settings.js46
-rw-r--r--models/trelloCreator.js19
-rw-r--r--models/users.js49
-rw-r--r--models/wekanCreator.js64
13 files changed, 1327 insertions, 262 deletions
diff --git a/models/accountSettings.js b/models/accountSettings.js
index db4087c0..6dfbac5d 100644
--- a/models/accountSettings.js
+++ b/models/accountSettings.js
@@ -23,11 +23,17 @@ AccountSettings.allow({
if (Meteor.isServer) {
Meteor.startup(() => {
- AccountSettings.upsert({ _id: 'accounts-allowEmailChange' }, {
+ AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, {
$setOnInsert: {
booleanValue: false,
sort: 0,
},
});
+ AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, {
+ $setOnInsert: {
+ booleanValue: false,
+ sort: 1,
+ },
+ });
});
}
diff --git a/models/activities.js b/models/activities.js
index 3f1d28ae..5b54759c 100644
--- a/models/activities.js
+++ b/models/activities.js
@@ -44,6 +44,12 @@ Activities.helpers({
checklistItem() {
return ChecklistItems.findOne(this.checklistItemId);
},
+ subtasks() {
+ return Cards.findOne(this.subtaskId);
+ },
+ customField() {
+ return CustomFields.findOne(this.customFieldId);
+ },
});
Activities.before.insert((userId, doc) => {
@@ -60,6 +66,7 @@ if (Meteor.isServer) {
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.after.insert((userId, doc) => {
@@ -127,6 +134,10 @@ if (Meteor.isServer) {
const checklistItem = activity.checklistItem();
params.checklistItem = checklistItem.title;
}
+ if (activity.customFieldId) {
+ const customField = activity.customField();
+ params.customField = customField.name;
+ }
if (board) {
const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId');
const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId');
diff --git a/models/attachments.js b/models/attachments.js
index 5e5c4926..91dd0dbc 100644
--- a/models/attachments.js
+++ b/models/attachments.js
@@ -1,90 +1,90 @@
- Attachments = new FS.Collection('attachments', {
- stores: [
+Attachments = new FS.Collection('attachments', {
+ stores: [
- // XXX Add a new store for cover thumbnails so we don't load big images in
- // the general board view
- new FS.Store.GridFS('attachments', {
- // If the uploaded document is not an image we need to enforce browser
- // download instead of execution. This is particularly important for HTML
- // files that the browser will just execute if we don't serve them with the
- // appropriate `application/octet-stream` MIME header which can lead to user
- // data leaks. I imagine other formats (like PDF) can also be attack vectors.
- // See https://github.com/wekan/wekan/issues/99
- // XXX Should we use `beforeWrite` option of CollectionFS instead of
- // collection-hooks?
- // We should use `beforeWrite`.
- beforeWrite: (fileObj) => {
- if (!fileObj.isImage()) {
- return {
- type: 'application/octet-stream',
- };
- }
- return {};
- },
- }),
- ],
- });
-
-
- if (Meteor.isServer) {
- Attachments.allow({
- insert(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- update(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- remove(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- // We authorize the attachment download either:
- // - if the board is public, everyone (even unconnected) can download it
- // - if the board is private, only board members can download it
- download(userId, doc) {
- const board = Boards.findOne(doc.boardId);
- if (board.isPublic()) {
- return true;
- } else {
- return board.hasMember(userId);
+ // XXX Add a new store for cover thumbnails so we don't load big images in
+ // the general board view
+ new FS.Store.GridFS('attachments', {
+ // If the uploaded document is not an image we need to enforce browser
+ // download instead of execution. This is particularly important for HTML
+ // files that the browser will just execute if we don't serve them with the
+ // appropriate `application/octet-stream` MIME header which can lead to user
+ // data leaks. I imagine other formats (like PDF) can also be attack vectors.
+ // See https://github.com/wekan/wekan/issues/99
+ // XXX Should we use `beforeWrite` option of CollectionFS instead of
+ // collection-hooks?
+ // We should use `beforeWrite`.
+ beforeWrite: (fileObj) => {
+ if (!fileObj.isImage()) {
+ return {
+ type: 'application/octet-stream',
+ };
}
+ return {};
},
+ }),
+ ],
+});
- fetch: ['boardId'],
- });
- }
-
- // XXX Enforce a schema for the Attachments CollectionFS
- if (Meteor.isServer) {
- Attachments.files.after.insert((userId, doc) => {
- // If the attachment doesn't have a source field
- // or its source is different than import
- if (!doc.source || doc.source !== 'import') {
- // Add activity about adding the attachment
- Activities.insert({
- userId,
- type: 'card',
- activityType: 'addAttachment',
- attachmentId: doc._id,
- boardId: doc.boardId,
- cardId: doc.cardId,
- });
+if (Meteor.isServer) {
+ Attachments.allow({
+ insert(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ update(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ remove(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ // We authorize the attachment download either:
+ // - if the board is public, everyone (even unconnected) can download it
+ // - if the board is private, only board members can download it
+ download(userId, doc) {
+ const board = Boards.findOne(doc.boardId);
+ if (board.isPublic()) {
+ return true;
} else {
- // Don't add activity about adding the attachment as the activity
- // be imported and delete source field
- Attachments.update({
- _id: doc._id,
- }, {
- $unset: {
- source: '',
- },
- });
+ return board.hasMember(userId);
}
- });
+ },
+
+ fetch: ['boardId'],
+ });
+}
- Attachments.files.after.remove((userId, doc) => {
- Activities.remove({
+// XXX Enforce a schema for the Attachments CollectionFS
+
+if (Meteor.isServer) {
+ Attachments.files.after.insert((userId, doc) => {
+ // If the attachment doesn't have a source field
+ // or its source is different than import
+ if (!doc.source || doc.source !== 'import') {
+ // Add activity about adding the attachment
+ Activities.insert({
+ userId,
+ type: 'card',
+ activityType: 'addAttachment',
attachmentId: doc._id,
+ boardId: doc.boardId,
+ cardId: doc.cardId,
});
+ } else {
+ // Don't add activity about adding the attachment as the activity
+ // be imported and delete source field
+ Attachments.update({
+ _id: doc._id,
+ }, {
+ $unset: {
+ source: '',
+ },
+ });
+ }
+ });
+
+ Attachments.files.after.remove((userId, doc) => {
+ Activities.remove({
+ attachmentId: doc._id,
});
- }
+ });
+}
diff --git a/models/boards.js b/models/boards.js
index 436a99f5..a017eb3f 100644
--- a/models/boards.js
+++ b/models/boards.js
@@ -31,14 +31,6 @@ Boards.attachSchema(new SimpleSchema({
}
},
},
- view: {
- type: String,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return 'board-view-lists';
- }
- },
- },
createdAt: {
type: Date,
autoValue() { // eslint-disable-line consistent-return
@@ -102,6 +94,9 @@ Boards.attachSchema(new SimpleSchema({
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-
@@ -156,6 +151,54 @@ Boards.attachSchema(new SimpleSchema({
type: String,
optional: true,
},
+ subtasksDefaultBoardId: {
+ type: String,
+ optional: true,
+ defaultValue: null,
+ },
+ subtasksDefaultListId: {
+ type: String,
+ optional: true,
+ defaultValue: null,
+ },
+ allowsSubtasks: {
+ type: Boolean,
+ defaultValue: true,
+ },
+ presentParentTask: {
+ 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: {
+ type: Date,
+ optional: true,
+ },
+ dueAt: {
+ type: Date,
+ optional: true,
+ },
+ endAt: {
+ type: Date,
+ optional: true,
+ },
+ spentTime: {
+ type: Number,
+ decimal: true,
+ optional: true,
+ },
+ isOvertime: {
+ type: Boolean,
+ defaultValue: false,
+ optional: true,
+ },
}));
@@ -191,6 +234,10 @@ Boards.helpers({
return this.permission === 'public';
},
+ cards() {
+ return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } });
+ },
+
lists() {
return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
},
@@ -257,6 +304,10 @@ Boards.helpers({
return `board-color-${this.color}`;
},
+ customFields() {
+ return CustomFields.find({ boardId: this._id }, { sort: { name: 1 } });
+ },
+
// XXX currently mutations return no value so we have an issue when using addLabel in import
// XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
pushLabel(name, color) {
@@ -265,28 +316,112 @@ Boards.helpers({
return _id;
},
- searchCards(term) {
+ searchCards(term, excludeLinked) {
check(term, Match.OneOf(String, null, undefined));
- let query = { boardId: this._id };
+ const query = { boardId: this._id };
+ if (excludeLinked) {
+ query.linkedId = null;
+ }
const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) {
const regex = new RegExp(term, 'i');
- query = {
- boardId: this._id,
- $or: [
- { title: regex },
- { description: regex },
- ],
- };
+ query.$or = [
+ { title: regex },
+ { description: regex },
+ ];
}
return Cards.find(query, projection);
},
+ // A board alwasy has another board where it deposits subtasks of thasks
+ // that belong to itself.
+ getDefaultSubtasksBoardId() {
+ 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}),
+ });
+
+ Swimlanes.insert({
+ title: TAPi18n.__('default'),
+ boardId: this.subtasksDefaultBoardId,
+ });
+ Boards.update(this._id, {$set: {
+ subtasksDefaultBoardId: this.subtasksDefaultBoardId,
+ }});
+ }
+ return this.subtasksDefaultBoardId;
+ },
+
+ getDefaultSubtasksBoard() {
+ return Boards.findOne(this.getDefaultSubtasksBoardId());
+ },
+
+ getDefaultSubtasksListId() {
+ if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) {
+ this.subtasksDefaultListId = Lists.insert({
+ title: TAPi18n.__('queue'),
+ boardId: this._id,
+ });
+ Boards.update(this._id, {$set: {
+ subtasksDefaultListId: this.subtasksDefaultListId,
+ }});
+ }
+ return this.subtasksDefaultListId;
+ },
+
+ getDefaultSubtasksList() {
+ return Lists.findOne(this.getDefaultSubtasksListId());
+ },
+
+ getDefaultSwimline() {
+ let result = Swimlanes.findOne({boardId: this._id});
+ if (result === undefined) {
+ Swimlanes.insert({
+ title: TAPi18n.__('default'),
+ boardId: this._id,
+ });
+ result = Swimlanes.findOne({boardId: this._id});
+ }
+ return result;
+ },
+
+ cardsInInterval(start, end) {
+ return Cards.find({
+ boardId: this._id,
+ $or: [
+ {
+ startAt: {
+ $lte: start,
+ }, endAt: {
+ $gte: start,
+ },
+ }, {
+ startAt: {
+ $lte: end,
+ }, endAt: {
+ $gte: end,
+ },
+ }, {
+ startAt: {
+ $gte: start,
+ }, endAt: {
+ $lte: end,
+ },
+ },
+ ],
+ });
+ },
+
});
+
Boards.mutations({
archive() {
return { $set: { archived: true } };
@@ -408,6 +543,22 @@ Boards.mutations({
},
};
},
+
+ setAllowsSubtasks(allowsSubtasks) {
+ return { $set: { allowsSubtasks } };
+ },
+
+ setSubtasksDefaultBoardId(subtasksDefaultBoardId) {
+ return { $set: { subtasksDefaultBoardId } };
+ },
+
+ setSubtasksDefaultListId(subtasksDefaultListId) {
+ return { $set: { subtasksDefaultListId } };
+ },
+
+ setPresentParentTask(presentParentTask) {
+ return { $set: { presentParentTask } };
+ },
});
if (Meteor.isServer) {
@@ -727,4 +878,33 @@ if (Meteor.isServer) {
});
}
});
+
+ JsonRoutes.add('PUT', '/api/boards/:id/labels', function (req, res) {
+ Authentication.checkUserId(req.userId);
+ const id = req.params.id;
+ try {
+ if (req.body.hasOwnProperty('label')) {
+ const board = Boards.findOne({ _id: id });
+ const color = req.body.label.color;
+ const name = req.body.label.name;
+ const labelId = Random.id(6);
+ if (!board.getLabel(name, color)) {
+ Boards.direct.update({ _id: id }, { $push: { labels: { _id: labelId, name, color } } });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: labelId,
+ });
+ } else {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ });
+ }
+ }
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ data: error,
+ });
+ }
+ });
}
diff --git a/models/cards.js b/models/cards.js
index 4a662953..302beddc 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -6,6 +6,8 @@ Cards = new Mongo.Collection('cards');
Cards.attachSchema(new SimpleSchema({
title: {
type: String,
+ optional: true,
+ defaultValue: '',
},
archived: {
type: Boolean,
@@ -15,8 +17,15 @@ Cards.attachSchema(new SimpleSchema({
}
},
},
+ parentId: {
+ type: String,
+ optional: true,
+ defaultValue: '',
+ },
listId: {
type: String,
+ optional: true,
+ defaultValue: '',
},
swimlaneId: {
type: String,
@@ -26,10 +35,14 @@ Cards.attachSchema(new SimpleSchema({
// difficult to manage and less efficient.
boardId: {
type: String,
+ optional: true,
+ defaultValue: '',
},
coverId: {
type: String,
optional: true,
+ defaultValue: '',
+
},
createdAt: {
type: Date,
@@ -41,6 +54,25 @@ Cards.attachSchema(new SimpleSchema({
}
},
},
+ customFields: {
+ type: [Object],
+ optional: true,
+ defaultValue: [],
+ },
+ 'customFields.$': {
+ type: new SimpleSchema({
+ _id: {
+ type: String,
+ optional: true,
+ defaultValue: '',
+ },
+ value: {
+ type: Match.OneOf(String, Number, Boolean, Date),
+ optional: true,
+ defaultValue: '',
+ },
+ }),
+ },
dateLastActivity: {
type: Date,
autoValue() {
@@ -50,14 +82,27 @@ Cards.attachSchema(new SimpleSchema({
description: {
type: String,
optional: true,
+ defaultValue: '',
+ },
+ requestedBy: {
+ type: String,
+ optional: true,
+ defaultValue: '',
+ },
+ assignedBy: {
+ type: String,
+ optional: true,
+ defaultValue: '',
},
labelIds: {
type: [String],
optional: true,
+ defaultValue: [],
},
members: {
type: [String],
optional: true,
+ defaultValue: [],
},
receivedAt: {
type: Date,
@@ -79,6 +124,7 @@ Cards.attachSchema(new SimpleSchema({
type: Number,
decimal: true,
optional: true,
+ defaultValue: 0,
},
isOvertime: {
type: Boolean,
@@ -98,6 +144,22 @@ Cards.attachSchema(new SimpleSchema({
sort: {
type: Number,
decimal: true,
+ defaultValue: '',
+ },
+ subtaskSort: {
+ type: Number,
+ decimal: true,
+ defaultValue: -1,
+ optional: true,
+ },
+ type: {
+ type: String,
+ defaultValue: '',
+ },
+ linkedId: {
+ type: String,
+ optional: true,
+ defaultValue: '',
},
}));
@@ -140,19 +202,33 @@ Cards.helpers({
},
isAssigned(memberId) {
- return _.contains(this.members, memberId);
+ return _.contains(this.getMembers(), memberId);
},
activities() {
- return Activities.find({cardId: this._id}, {sort: {createdAt: -1}});
+ if (this.isLinkedCard()) {
+ return Activities.find({cardId: this.linkedId}, {sort: {createdAt: -1}});
+ } else if (this.isLinkedBoard()) {
+ return Activities.find({boardId: this.linkedId}, {sort: {createdAt: -1}});
+ } else {
+ return Activities.find({cardId: this._id}, {sort: {createdAt: -1}});
+ }
},
comments() {
- return CardComments.find({cardId: this._id}, {sort: {createdAt: -1}});
+ if (this.isLinkedCard()) {
+ return CardComments.find({cardId: this.linkedId}, {sort: {createdAt: -1}});
+ } else {
+ return CardComments.find({cardId: this._id}, {sort: {createdAt: -1}});
+ }
},
attachments() {
- return Attachments.find({cardId: this._id}, {sort: {uploadedAt: -1}});
+ if (this.isLinkedCard()) {
+ return Attachments.find({cardId: this.linkedId}, {sort: {uploadedAt: -1}});
+ } else {
+ return Attachments.find({cardId: this._id}, {sort: {uploadedAt: -1}});
+ }
},
cover() {
@@ -163,7 +239,11 @@ Cards.helpers({
},
checklists() {
- return Checklists.find({cardId: this._id}, {sort: { sort: 1 } });
+ if (this.isLinkedCard()) {
+ return Checklists.find({cardId: this.linkedId}, {sort: { sort: 1 } });
+ } else {
+ return Checklists.find({cardId: this._id}, {sort: { sort: 1 } });
+ }
},
checklistItemCount() {
@@ -192,6 +272,82 @@ Cards.helpers({
return this.checklistItemCount() !== 0;
},
+ subtasks() {
+ return Cards.find({
+ parentId: this._id,
+ archived: false,
+ }, {sort: { sort: 1 } });
+ },
+
+ allSubtasks() {
+ return Cards.find({
+ parentId: this._id,
+ archived: false,
+ }, {sort: { sort: 1 } });
+ },
+
+ subtasksCount() {
+ return Cards.find({
+ parentId: this._id,
+ archived: false,
+ }).count();
+ },
+
+ subtasksFinishedCount() {
+ return Cards.find({
+ parentId: this._id,
+ archived: true}).count();
+ },
+
+ subtasksFinished() {
+ const finishCount = this.subtasksFinishedCount();
+ return finishCount > 0 && this.subtasksCount() === finishCount;
+ },
+
+ allowsSubtasks() {
+ return this.subtasksCount() !== 0;
+ },
+
+ customFieldIndex(customFieldId) {
+ return _.pluck(this.customFields, '_id').indexOf(customFieldId);
+ },
+
+ // customFields with definitions
+ customFieldsWD() {
+
+ // get all definitions
+ const definitions = CustomFields.find({
+ boardId: this.boardId,
+ }).fetch();
+
+ // match right definition to each field
+ if (!this.customFields) return [];
+ return this.customFields.map((customField) => {
+ const definition = definitions.find((definition) => {
+ return definition._id === customField._id;
+ });
+ //search for "True Value" which is for DropDowns other then the Value (which is the id)
+ let trueValue = customField.value;
+ if (definition.settings.dropdownItems && definition.settings.dropdownItems.length > 0)
+ {
+ for (let i = 0; i < definition.settings.dropdownItems.length; i++)
+ {
+ if (definition.settings.dropdownItems[i]._id === customField.value)
+ {
+ trueValue = definition.settings.dropdownItems[i].name;
+ }
+ }
+ }
+ return {
+ _id: customField._id,
+ value: customField.value,
+ trueValue,
+ definition,
+ };
+ });
+
+ },
+
absoluteUrl() {
const board = this.board();
return FlowRouter.url('card', {
@@ -208,119 +364,550 @@ Cards.helpers({
}
return true;
},
-});
-Cards.mutations({
- archive() {
- return {$set: {archived: true}};
+ parentCard() {
+ if (this.parentId === '') {
+ return null;
+ }
+ return Cards.findOne(this.parentId);
},
- restore() {
- return {$set: {archived: false}};
+ parentCardName() {
+ let result = '';
+ if (this.parentId !== '') {
+ const card = Cards.findOne(this.parentId);
+ if (card) {
+ result = card.title;
+ }
+ }
+ return result;
},
- setTitle(title) {
- return {$set: {title}};
+ parentListId() {
+ const result = [];
+ let crtParentId = this.parentId;
+ while (crtParentId !== '') {
+ const crt = Cards.findOne(crtParentId);
+ if ((crt === null) || (crt === undefined)) {
+ // maybe it has been deleted
+ break;
+ }
+ if (crtParentId in result) {
+ // circular reference
+ break;
+ }
+ result.unshift(crtParentId);
+ crtParentId = crt.parentId;
+ }
+ return result;
},
- setDescription(description) {
- return {$set: {description}};
+ parentList() {
+ const resultId = [];
+ const result = [];
+ let crtParentId = this.parentId;
+ while (crtParentId !== '') {
+ const crt = Cards.findOne(crtParentId);
+ if ((crt === null) || (crt === undefined)) {
+ // maybe it has been deleted
+ break;
+ }
+ if (crtParentId in resultId) {
+ // circular reference
+ break;
+ }
+ resultId.unshift(crtParentId);
+ result.unshift(crt);
+ crtParentId = crt.parentId;
+ }
+ return result;
},
- move(swimlaneId, listId, sortIndex) {
- const list = Lists.findOne(listId);
- const mutatedFields = {
- swimlaneId,
- listId,
- boardId: list.boardId,
- sort: sortIndex,
- };
+ parentString(sep) {
+ return this.parentList().map(function(elem){
+ return elem.title;
+ }).join(sep);
+ },
- return {$set: mutatedFields};
+ isTopLevel() {
+ return this.parentId === '';
},
- addLabel(labelId) {
- return {$addToSet: {labelIds: labelId}};
+ isLinkedCard() {
+ return this.type === 'cardType-linkedCard';
},
- removeLabel(labelId) {
- return {$pull: {labelIds: labelId}};
+ isLinkedBoard() {
+ return this.type === 'cardType-linkedBoard';
},
- toggleLabel(labelId) {
- if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
- return this.removeLabel(labelId);
+ isLinked() {
+ return this.isLinkedCard() || this.isLinkedBoard();
+ },
+
+ setDescription(description) {
+ if (this.isLinkedCard()) {
+ return Cards.update({_id: this.linkedId}, {$set: {description}});
+ } else if (this.isLinkedBoard()) {
+ return Boards.update({_id: this.linkedId}, {$set: {description}});
} else {
- return this.addLabel(labelId);
+ return Cards.update(
+ {_id: this._id},
+ {$set: {description}}
+ );
+ }
+ },
+
+ getDescription() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({_id: this.linkedId});
+ if (card && card.description)
+ return card.description;
+ else
+ return null;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({_id: this.linkedId});
+ if (board && board.description)
+ return board.description;
+ else
+ return null;
+ } else if (this.description) {
+ return this.description;
+ } else {
+ return null;
+ }
+ },
+
+ getMembers() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({_id: this.linkedId});
+ return card.members;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({_id: this.linkedId});
+ return board.activeMembers().map((member) => {
+ return member.userId;
+ });
+ } else {
+ return this.members;
}
},
assignMember(memberId) {
- return {$addToSet: {members: memberId}};
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ { $addToSet: { members: memberId }}
+ );
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({_id: this.linkedId});
+ return board.addMember(memberId);
+ } else {
+ return Cards.update(
+ { _id: this._id },
+ { $addToSet: { members: memberId}}
+ );
+ }
},
unassignMember(memberId) {
- return {$pull: {members: memberId}};
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ { $pull: { members: memberId }}
+ );
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({_id: this.linkedId});
+ return board.removeMember(memberId);
+ } else {
+ return Cards.update(
+ { _id: this._id },
+ { $pull: { members: memberId}}
+ );
+ }
},
toggleMember(memberId) {
- if (this.members && this.members.indexOf(memberId) > -1) {
+ if (this.getMembers() && this.getMembers().indexOf(memberId) > -1) {
return this.unassignMember(memberId);
} else {
return this.assignMember(memberId);
}
},
- setCover(coverId) {
- return {$set: {coverId}};
- },
-
- unsetCover() {
- return {$unset: {coverId: ''}};
+ getReceived() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({_id: this.linkedId});
+ return card.receivedAt;
+ } else {
+ return this.receivedAt;
+ }
},
setReceived(receivedAt) {
- return {$set: {receivedAt}};
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ {_id: this.linkedId},
+ {$set: {receivedAt}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {receivedAt}}
+ );
+ }
},
- unsetReceived() {
- return {$unset: {receivedAt: ''}};
+ getStart() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({_id: this.linkedId});
+ return card.startAt;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({_id: this.linkedId});
+ return board.startAt;
+ } else {
+ return this.startAt;
+ }
},
setStart(startAt) {
- return {$set: {startAt}};
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {startAt}}
+ );
+ } else if (this.isLinkedBoard()) {
+ return Boards.update(
+ {_id: this.linkedId},
+ {$set: {startAt}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {startAt}}
+ );
+ }
},
- unsetStart() {
- return {$unset: {startAt: ''}};
+ getDue() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({_id: this.linkedId});
+ return card.dueAt;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({_id: this.linkedId});
+ return board.dueAt;
+ } else {
+ return this.dueAt;
+ }
},
setDue(dueAt) {
- return {$set: {dueAt}};
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {dueAt}}
+ );
+ } else if (this.isLinkedBoard()) {
+ return Boards.update(
+ {_id: this.linkedId},
+ {$set: {dueAt}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {dueAt}}
+ );
+ }
},
- unsetDue() {
- return {$unset: {dueAt: ''}};
+ getEnd() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({_id: this.linkedId});
+ return card.endAt;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({_id: this.linkedId});
+ return board.endAt;
+ } else {
+ return this.endAt;
+ }
},
setEnd(endAt) {
- return {$set: {endAt}};
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {endAt}}
+ );
+ } else if (this.isLinkedBoard()) {
+ return Boards.update(
+ {_id: this.linkedId},
+ {$set: {endAt}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {endAt}}
+ );
+ }
},
- unsetEnd() {
- return {$unset: {endAt: ''}};
+ getIsOvertime() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.isOvertime;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId});
+ return board.isOvertime;
+ } else {
+ return this.isOvertime;
+ }
+ },
+
+ setIsOvertime(isOvertime) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {isOvertime}}
+ );
+ } else if (this.isLinkedBoard()) {
+ return Boards.update(
+ {_id: this.linkedId},
+ {$set: {isOvertime}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {isOvertime}}
+ );
+ }
},
- setOvertime(isOvertime) {
- return {$set: {isOvertime}};
+ getSpentTime() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.spentTime;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId});
+ return board.spentTime;
+ } else {
+ return this.spentTime;
+ }
},
setSpentTime(spentTime) {
- return {$set: {spentTime}};
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {spentTime}}
+ );
+ } else if (this.isLinkedBoard()) {
+ return Boards.update(
+ {_id: this.linkedId},
+ {$set: {spentTime}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {spentTime}}
+ );
+ }
+ },
+
+ getId() {
+ if (this.isLinked()) {
+ return this.linkedId;
+ } else {
+ return this._id;
+ }
+ },
+
+ getTitle() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.title;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId});
+ return board.title;
+ } else {
+ return this.title;
+ }
+ },
+
+ getBoardTitle() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ const board = Boards.findOne({ _id: card.boardId });
+ return board.title;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId});
+ return board.title;
+ } else {
+ const board = Boards.findOne({ _id: this.boardId });
+ return board.title;
+ }
+ },
+
+ setTitle(title) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {title}}
+ );
+ } else if (this.isLinkedBoard()) {
+ return Boards.update(
+ {_id: this.linkedId},
+ {$set: {title}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {title}}
+ );
+ }
+ },
+
+ getArchived() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.archived;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId});
+ return board.archived;
+ } else {
+ return this.archived;
+ }
+ },
+
+ setRequestedBy(requestedBy) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {requestedBy}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {requestedBy}}
+ );
+ }
},
- unsetSpentTime() {
- return {$unset: {spentTime: '', isOvertime: false}};
+ getRequestedBy() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.requestedBy;
+ } else {
+ return this.requestedBy;
+ }
+ },
+
+ setAssignedBy(assignedBy) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ {$set: {assignedBy}}
+ );
+ } else {
+ return Cards.update(
+ {_id: this._id},
+ {$set: {assignedBy}}
+ );
+ }
+ },
+
+ getAssignedBy() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.assignedBy;
+ } else {
+ return this.assignedBy;
+ }
+ },
+});
+
+Cards.mutations({
+ applyToChildren(funct) {
+ Cards.find({ parentId: this._id }).forEach((card) => {
+ funct(card);
+ });
+ },
+
+ archive() {
+ this.applyToChildren((card) => { return card.archive(); });
+ return {$set: {archived: true}};
+ },
+
+ restore() {
+ this.applyToChildren((card) => { return card.restore(); });
+ return {$set: {archived: false}};
+ },
+
+ move(swimlaneId, listId, sortIndex) {
+ const list = Lists.findOne(listId);
+ const mutatedFields = {
+ swimlaneId,
+ listId,
+ boardId: list.boardId,
+ sort: sortIndex,
+ };
+
+ return {$set: mutatedFields};
+ },
+
+ addLabel(labelId) {
+ return {$addToSet: {labelIds: labelId}};
+ },
+
+ removeLabel(labelId) {
+ return {$pull: {labelIds: labelId}};
+ },
+
+ toggleLabel(labelId) {
+ if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
+ return this.removeLabel(labelId);
+ } else {
+ return this.addLabel(labelId);
+ }
+ },
+
+ assignCustomField(customFieldId) {
+ return {$addToSet: {customFields: {_id: customFieldId, value: null}}};
+ },
+
+ unassignCustomField(customFieldId) {
+ return {$pull: {customFields: {_id: customFieldId}}};
+ },
+
+ toggleCustomField(customFieldId) {
+ if (this.customFields && this.customFieldIndex(customFieldId) > -1) {
+ return this.unassignCustomField(customFieldId);
+ } else {
+ return this.assignCustomField(customFieldId);
+ }
+ },
+
+ setCustomField(customFieldId, value) {
+ // todo
+ const index = this.customFieldIndex(customFieldId);
+ if (index > -1) {
+ const update = {$set: {}};
+ update.$set[`customFields.${index}.value`] = value;
+ return update;
+ }
+ // TODO
+ // Ignatz 18.05.2018: Return null to silence ESLint. No Idea if that is correct
+ return null;
+ },
+
+ setCover(coverId) {
+ return {$set: {coverId}};
+ },
+
+ unsetCover() {
+ return {$unset: {coverId: ''}};
+ },
+
+ setParentId(parentId) {
+ return {$set: {parentId}};
},
});
@@ -413,6 +1000,9 @@ function cardRemover(userId, doc) {
Checklists.remove({
cardId: doc._id,
});
+ Subtasks.remove({
+ cardId: doc._id,
+ });
CardComments.remove({
cardId: doc._id,
});
@@ -489,6 +1079,7 @@ if (Meteor.isServer) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
const check = Users.findOne({_id: req.body.authorId});
+ const members = req.body.members || [req.body.authorId];
if (typeof check !== 'undefined') {
const id = Cards.direct.insert({
title: req.body.title,
@@ -498,7 +1089,7 @@ if (Meteor.isServer) {
userId: req.body.authorId,
swimlaneId: req.body.swimlaneId,
sort: 0,
- members: [req.body.authorId],
+ members,
});
JsonRoutes.sendResult(res, {
code: 200,
@@ -542,6 +1133,11 @@ if (Meteor.isServer) {
Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
{$set: {description: newDescription}});
}
+ if (req.body.hasOwnProperty('labelIds')) {
+ const newlabelIds = req.body.labelIds;
+ Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
+ {$set: {labelIds: newlabelIds}});
+ }
JsonRoutes.sendResult(res, {
code: 200,
data: {
diff --git a/models/checklistItems.js b/models/checklistItems.js
index 3c01d476..e075eda2 100644
--- a/models/checklistItems.js
+++ b/models/checklistItems.js
@@ -93,3 +93,53 @@ if (Meteor.isServer) {
itemRemover(userId, doc);
});
}
+
+if (Meteor.isServer) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
+ Authentication.checkUserId( req.userId);
+ const paramItemId = req.params.itemId;
+ const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
+ if (checklistItem) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: checklistItem,
+ });
+ } else {
+ JsonRoutes.sendResult(res, {
+ code: 500,
+ });
+ }
+ });
+
+ JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
+ Authentication.checkUserId( req.userId);
+
+ const paramItemId = req.params.itemId;
+
+ if (req.body.hasOwnProperty('isFinished')) {
+ ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}});
+ }
+ if (req.body.hasOwnProperty('title')) {
+ ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}});
+ }
+
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramItemId,
+ },
+ });
+ });
+
+ JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
+ Authentication.checkUserId( req.userId);
+ const paramItemId = req.params.itemId;
+ ChecklistItems.direct.remove({ _id: paramItemId });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramItemId,
+ },
+ });
+ });
+}
diff --git a/models/checklists.js b/models/checklists.js
index 9946f98e..c58453ef 100644
--- a/models/checklists.js
+++ b/models/checklists.js
@@ -34,9 +34,9 @@ Checklists.helpers({
return ChecklistItems.find({ checklistId: this._id }).count();
},
items() {
- return ChecklistItems.find(Filter.mongoSelector({
+ return ChecklistItems.find({
checklistId: this._id,
- }), { sort: ['sort'] });
+ }, { sort: ['sort'] });
},
finishedCount() {
return ChecklistItems.find({
@@ -106,94 +106,90 @@ if (Meteor.isServer) {
if (Meteor.isServer) {
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramCardId = req.params.cardId;
+ 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.find({ cardId: paramCardId }).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- };
- }),
+ data: checklists,
});
- }
- catch (error) {
+ } else {
JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
+ code: 500,
});
}
});
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramChecklistId = req.params.checklistId;
- const paramCardId = req.params.cardId;
+ 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: Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }),
+ data: checklist,
});
- }
- catch (error) {
+ } else {
JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
+ code: 500,
});
}
});
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramCardId = req.params.cardId;
-
- const checklistToSend = {};
- checklistToSend.cardId = paramCardId;
- checklistToSend.title = req.body.title;
- checklistToSend.items = [];
- const id = Checklists.insert(checklistToSend);
- const checklist = Checklists.findOne({_id: id});
- req.body.items.forEach(function (item) {
- checklist.addItem(item);
- }, this);
-
+ 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,
},
});
- }
- catch (error) {
+ } else {
JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
+ code: 400,
});
}
});
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramCommentId = req.params.commentId;
- const paramCardId = req.params.cardId;
- Checklists.remove({ _id: paramCommentId, cardId: paramCardId });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramCardId,
- },
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
- }
+ Authentication.checkUserId( req.userId);
+ const paramChecklistId = req.params.checklistId;
+ Checklists.remove({ _id: paramChecklistId });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramChecklistId,
+ },
+ });
});
}
diff --git a/models/customFields.js b/models/customFields.js
new file mode 100644
index 00000000..6c5fe7c4
--- /dev/null
+++ b/models/customFields.js
@@ -0,0 +1,132 @@
+CustomFields = new Mongo.Collection('customFields');
+
+CustomFields.attachSchema(new SimpleSchema({
+ boardId: {
+ type: String,
+ },
+ name: {
+ type: String,
+ },
+ type: {
+ type: String,
+ allowedValues: ['text', 'number', 'date', 'dropdown'],
+ },
+ settings: {
+ type: Object,
+ },
+ 'settings.dropdownItems': {
+ type: [Object],
+ optional: true,
+ },
+ 'settings.dropdownItems.$': {
+ type: new SimpleSchema({
+ _id: {
+ type: String,
+ },
+ name: {
+ type: String,
+ },
+ }),
+ },
+ showOnCard: {
+ type: Boolean,
+ },
+}));
+
+CustomFields.allow({
+ insert(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ update(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ remove(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ fetch: ['userId', 'boardId'],
+});
+
+// not sure if we need this?
+//CustomFields.hookOptions.after.update = { fetchPrevious: false };
+
+function customFieldCreation(userId, doc){
+ Activities.insert({
+ userId,
+ activityType: 'createCustomField',
+ boardId: doc.boardId,
+ customFieldId: doc._id,
+ });
+}
+
+if (Meteor.isServer) {
+ /*Meteor.startup(() => {
+ CustomFields._collection._ensureIndex({ boardId: 1});
+ });*/
+
+ CustomFields.after.insert((userId, doc) => {
+ customFieldCreation(userId, doc);
+ });
+
+ CustomFields.after.remove((userId, doc) => {
+ Activities.remove({
+ customFieldId: doc._id,
+ });
+ });
+}
+
+//CUSTOM FIELD REST API
+if (Meteor.isServer) {
+ 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({ boardId: 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, boardId: paramBoardId }),
+ });
+ });
+
+ 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,
+ type: req.body.type,
+ settings: req.body.settings,
+ showOnCard: req.body.showOnCard,
+ boardId: paramBoardId,
+ });
+
+ const customField = CustomFields.findOne({_id: id, boardId: paramBoardId });
+ customFieldCreation(req.body.authorId, customField);
+
+ 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, boardId: paramBoardId });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ });
+}
diff --git a/models/export.js b/models/export.js
index c6632198..ed4c52d9 100644
--- a/models/export.js
+++ b/models/export.js
@@ -45,6 +45,7 @@ class Exporter {
build() {
const byBoard = { boardId: this._boardId };
+ const byBoardNoLinked = { boardId: this._boardId, linkedId: null };
// we do not want to retrieve boardId in related elements
const noBoardId = { fields: { boardId: 0 } };
const result = {
@@ -52,14 +53,19 @@ class Exporter {
};
_.extend(result, Boards.findOne(this._boardId, { fields: { stars: 0 } }));
result.lists = Lists.find(byBoard, noBoardId).fetch();
- result.cards = Cards.find(byBoard, noBoardId).fetch();
+ result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
result.comments = CardComments.find(byBoard, noBoardId).fetch();
result.activities = Activities.find(byBoard, noBoardId).fetch();
result.checklists = [];
+ result.checklistItems = [];
+ result.subtaskItems = [];
result.cards.forEach((card) => {
result.checklists.push(...Checklists.find({ cardId: card._id }).fetch());
+ result.checklistItems.push(...ChecklistItems.find({ cardId: card._id }).fetch());
+ result.subtaskItems.push(...Cards.find({ parentid: card._id }).fetch());
});
+
// [Old] for attachments we only export IDs and absolute url to original doc
// [New] Encode attachment to base64
const getBase64Data = function(doc, callback) {
diff --git a/models/settings.js b/models/settings.js
index 34f693d9..3b9b4eae 100644
--- a/models/settings.js
+++ b/models/settings.js
@@ -96,6 +96,14 @@ if (Meteor.isServer) {
return (min + Math.round(rand * range));
}
+ function getEnvVar(name){
+ const value = process.env[name];
+ if (value){
+ return value;
+ }
+ throw new Meteor.Error(['var-not-exist', `The environment variable ${name} does not exist`]);
+ }
+
function sendInvitationEmail (_id){
const icode = InvitationCodes.findOne(_id);
const author = Users.findOne(Meteor.userId());
@@ -124,20 +132,33 @@ if (Meteor.isServer) {
sendInvitation(emails, boards) {
check(emails, [String]);
check(boards, [String]);
+
const user = Users.findOne(Meteor.userId());
if(!user.isAdmin){
throw new Meteor.Error('not-allowed');
}
emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email)) {
- 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);
- }
- });
+ // 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.`);
+ }
+ // Checks if the email is already link to an invitation.
+ const invitation = InvitationCodes.findOne({email});
+ if (invitation){
+ InvitationCodes.update(invitation, {$set : {boardsToBeInvited: boards}});
+ sendInvitationEmail(invitation._id);
+ }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);
+ }
+ });
+ }
}
});
},
@@ -167,5 +188,14 @@ if (Meteor.isServer) {
email: user.emails[0].address,
};
},
+
+ getMatomoConf(){
+ return {
+ address: getEnvVar('MATOMO_ADDRESS'),
+ siteId: getEnvVar('MATOMO_SITE_ID'),
+ doNotTrack: process.env.MATOMO_DO_NOT_TRACK || false,
+ withUserName: process.env.MATOMO_WITH_USERNAME || false,
+ };
+ },
});
}
diff --git a/models/trelloCreator.js b/models/trelloCreator.js
index 89e48a16..30f0bc2b 100644
--- a/models/trelloCreator.js
+++ b/models/trelloCreator.js
@@ -379,6 +379,7 @@ export class TrelloCreator {
// we require.
createdAt: this._now(this.createdAt.lists[list.id]),
title: list.name,
+ sort: list.pos,
};
const listId = Lists.direct.insert(listToCreate);
Lists.direct.update(listId, {$set: {'updatedAt': this._now()}});
@@ -410,6 +411,7 @@ export class TrelloCreator {
// we require.
createdAt: this._now(),
title: 'Default',
+ sort: 1,
};
const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
Swimlanes.direct.update(swimlaneId, {$set: {'updatedAt': this._now()}});
@@ -429,17 +431,20 @@ export class TrelloCreator {
const checklistId = Checklists.direct.insert(checklistToCreate);
// keep track of Trello id => WeKan id
this.checklists[checklist.id] = checklistId;
- // Now add the items to the checklist
- const itemsToCreate = [];
+ // Now add the items to the checklistItems
+ let counter = 0;
checklist.checkItems.forEach((item) => {
- itemsToCreate.push({
- _id: checklistId + itemsToCreate.length,
+ counter++;
+ const checklistItemTocreate = {
+ _id: checklistId + counter,
title: item.name,
- isFinished: item.state === 'complete',
+ checklistId: this.checklists[checklist.id],
+ cardId: this.cards[checklist.idCard],
sort: item.pos,
- });
+ isFinished: item.state === 'complete',
+ };
+ ChecklistItems.direct.insert(checklistItemTocreate);
});
- Checklists.direct.update(checklistId, {$set: {items: itemsToCreate}});
}
});
}
diff --git a/models/users.js b/models/users.js
index a04021c1..6e83337e 100644
--- a/models/users.js
+++ b/models/users.js
@@ -43,7 +43,9 @@ Users.attachSchema(new SimpleSchema({
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
- return {};
+ return {
+ boardView: 'board-view-lists',
+ };
}
},
},
@@ -95,6 +97,15 @@ Users.attachSchema(new SimpleSchema({
type: String,
optional: true,
},
+ 'profile.boardView': {
+ type: String,
+ optional: true,
+ allowedValues: [
+ 'board-view-lists',
+ 'board-view-swimlanes',
+ 'board-view-cal',
+ ],
+ },
services: {
type: Object,
optional: true,
@@ -329,6 +340,14 @@ Users.mutations({
setShowCardsCountAt(limit) {
return {$set: {'profile.showCardsCountAt': limit}};
},
+
+ setBoardView(view) {
+ return {
+ $set : {
+ 'profile.boardView': view,
+ },
+ };
+ },
});
Meteor.methods({
@@ -508,9 +527,14 @@ if (Meteor.isServer) {
throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist');
} else {
user.profile = {icode: options.profile.invitationcode};
- }
+ user.profile.boardView = 'board-view-lists';
- return user;
+ // Deletes the invitation code after the user was created successfully.
+ setTimeout(Meteor.bindEnvironment(() => {
+ InvitationCodes.remove({'_id': invitationCode._id});
+ }), 200);
+ return user;
+ }
});
}
@@ -579,10 +603,11 @@ if (Meteor.isServer) {
Swimlanes.insert({
title: TAPi18n.__('welcome-swimlane'),
boardId,
+ sort: 1,
}, fakeUser);
- ['welcome-list1', 'welcome-list2'].forEach((title) => {
- Lists.insert({title: TAPi18n.__(title), boardId}, fakeUser);
+ ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
+ Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
});
});
});
@@ -624,9 +649,20 @@ if (Meteor.isServer) {
});
}
-
// USERS REST API
if (Meteor.isServer) {
+ // Middleware which checks that API is enabled.
+ JsonRoutes.Middleware.use(function (req, res, next) {
+ const api = req.url.search('api');
+ if (api === 1 && process.env.WITH_API === 'true' || api === -1){
+ return next();
+ }
+ else {
+ res.writeHead(301, {Location: '/'});
+ return res.end();
+ }
+ });
+
JsonRoutes.add('GET', '/api/user', function(req, res) {
try {
Authentication.checkLoggedIn(req.userId);
@@ -767,4 +803,3 @@ if (Meteor.isServer) {
}
});
}
-
diff --git a/models/wekanCreator.js b/models/wekanCreator.js
index 99d1df2d..4551979b 100644
--- a/models/wekanCreator.js
+++ b/models/wekanCreator.js
@@ -36,6 +36,8 @@ export class WekanCreator {
this.attachmentIds = {};
// Map of checklists Wekan ID => Wekan ID
this.checklists = {};
+ // Map of checklistItems Wekan ID => Wekan ID
+ this.checklistItems = {};
// The comments, indexed by Wekan card id (to map when importing cards)
this.comments = {};
// the members, indexed by Wekan member id => Wekan user ID
@@ -135,10 +137,13 @@ export class WekanCreator {
check(wekanChecklists, [Match.ObjectIncluding({
cardId: String,
title: String,
- items: [Match.ObjectIncluding({
- isFinished: Boolean,
- title: String,
- })],
+ })]);
+ }
+
+ checkChecklistItems(wekanChecklistItems) {
+ check(wekanChecklistItems, [Match.ObjectIncluding({
+ cardId: String,
+ title: String,
})]);
}
@@ -385,7 +390,7 @@ export class WekanCreator {
}
createLists(wekanLists, boardId) {
- wekanLists.forEach((list) => {
+ wekanLists.forEach((list, listIndex) => {
const listToCreate = {
archived: list.archived,
boardId,
@@ -395,6 +400,7 @@ export class WekanCreator {
// we require.
createdAt: this._now(this.createdAt.lists[list.id]),
title: list.title,
+ sort: list.sort ? list.sort : listIndex,
};
const listId = Lists.direct.insert(listToCreate);
Lists.direct.update(listId, {$set: {'updatedAt': this._now()}});
@@ -417,7 +423,7 @@ export class WekanCreator {
}
createSwimlanes(wekanSwimlanes, boardId) {
- wekanSwimlanes.forEach((swimlane) => {
+ wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
const swimlaneToCreate = {
archived: swimlane.archived,
boardId,
@@ -427,6 +433,7 @@ export class WekanCreator {
// we require.
createdAt: this._now(this.createdAt.swimlanes[swimlane._id]),
title: swimlane.title,
+ sort: swimlane.sort ? swimlane.sort : swimlaneIndex,
};
const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
Swimlanes.direct.update(swimlaneId, {$set: {'updatedAt': this._now()}});
@@ -435,6 +442,7 @@ export class WekanCreator {
}
createChecklists(wekanChecklists) {
+ const result = [];
wekanChecklists.forEach((checklist, checklistIndex) => {
// Create the checklist
const checklistToCreate = {
@@ -444,19 +452,24 @@ export class WekanCreator {
sort: checklist.sort ? checklist.sort : checklistIndex,
};
const checklistId = Checklists.direct.insert(checklistToCreate);
- // keep track of Wekan id => WeKan id
this.checklists[checklist._id] = checklistId;
- // Now add the items to the checklist
- const itemsToCreate = [];
- checklist.items.forEach((item, itemIndex) => {
- itemsToCreate.push({
- _id: checklistId + itemsToCreate.length,
- title: item.title,
- isFinished: item.isFinished,
- sort: item.sort ? item.sort : itemIndex,
- });
- });
- Checklists.direct.update(checklistId, {$set: {items: itemsToCreate}});
+ result.push(checklistId);
+ });
+ return result;
+ }
+
+ createChecklistItems(wekanChecklistItems) {
+ wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => {
+ // Create the checklistItem
+ const checklistItemTocreate = {
+ title: checklistitem.title,
+ checklistId: this.checklists[checklistitem.checklistId],
+ cardId: this.cards[checklistitem.cardId],
+ sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex,
+ isFinished: checklistitem.isFinished,
+ };
+ const checklistItemId = ChecklistItems.direct.insert(checklistItemTocreate);
+ this.checklistItems[checklistitem._id] = checklistItemId;
});
}
@@ -470,14 +483,17 @@ export class WekanCreator {
const wekanAttachment = wekanBoard.attachments.filter((attachment) => {
return attachment._id === activity.attachmentId;
})[0];
- if(wekanAttachment.url || wekanAttachment.file) {
+
+ if ( typeof wekanAttachment !== 'undefined' && wekanAttachment ) {
+ if(wekanAttachment.url || wekanAttachment.file) {
// we cannot actually create the Wekan attachment, because we don't yet
// have the cards to attach it to, so we store it in the instance variable.
- const wekanCardId = activity.cardId;
- if(!this.attachments[wekanCardId]) {
- this.attachments[wekanCardId] = [];
+ const wekanCardId = activity.cardId;
+ if(!this.attachments[wekanCardId]) {
+ this.attachments[wekanCardId] = [];
+ }
+ this.attachments[wekanCardId].push(wekanAttachment);
}
- this.attachments[wekanCardId].push(wekanAttachment);
}
break;
}
@@ -635,6 +651,7 @@ export class WekanCreator {
this.checkSwimlanes(board.swimlanes);
this.checkCards(board.cards);
this.checkChecklists(board.checklists);
+ this.checkChecklistItems(board.checklistItems);
} catch (e) {
throw new Meteor.Error('error-json-schema');
}
@@ -654,6 +671,7 @@ export class WekanCreator {
this.createSwimlanes(board.swimlanes, boardId);
this.createCards(board.cards, boardId);
this.createChecklists(board.checklists);
+ this.createChecklistItems(board.checklistItems);
this.importActivities(board.activities, boardId);
// XXX add members
return boardId;