summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
Diffstat (limited to 'models')
-rw-r--r--models/activities.js8
-rw-r--r--models/attachments.js158
-rw-r--r--models/boards.js4
-rw-r--r--models/cards.js69
-rw-r--r--models/customFields.js132
5 files changed, 292 insertions, 79 deletions
diff --git a/models/activities.js b/models/activities.js
index 3f1d28ae..f64b53f8 100644
--- a/models/activities.js
+++ b/models/activities.js
@@ -44,6 +44,9 @@ Activities.helpers({
checklistItem() {
return ChecklistItems.findOne(this.checklistItemId);
},
+ customField() {
+ return CustomFields.findOne(this.customFieldId);
+ },
});
Activities.before.insert((userId, doc) => {
@@ -60,6 +63,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 +131,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 c863c5ce..44ce0b62 100644
--- a/models/boards.js
+++ b/models/boards.js
@@ -249,6 +249,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) {
diff --git a/models/cards.js b/models/cards.js
index 01f79847..c77cd682 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -41,6 +41,21 @@ Cards.attachSchema(new SimpleSchema({
}
},
},
+ customFields: {
+ type: [Object],
+ optional: true,
+ },
+ 'customFields.$': {
+ type: new SimpleSchema({
+ _id: {
+ type: String,
+ },
+ value: {
+ type: Match.OneOf(String, Number, Boolean, Date),
+ optional: true,
+ },
+ }),
+ },
dateLastActivity: {
type: Date,
autoValue() {
@@ -192,6 +207,31 @@ Cards.helpers({
return this.checklistItemCount() !== 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
+ return this.customFields.map((customField) => {
+ return {
+ _id: customField._id,
+ value: customField.value,
+ definition: definitions.find((definition) => {
+ return definition._id === customField._id;
+ }),
+ };
+ });
+
+ },
+
absoluteUrl() {
const board = this.board();
return FlowRouter.url('card', {
@@ -271,6 +311,35 @@ Cards.mutations({
}
},
+ 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}};
},
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,
+ },
+ });
+ });
+}