summaryrefslogtreecommitdiffstats
path: root/collections/boards.js
diff options
context:
space:
mode:
Diffstat (limited to 'collections/boards.js')
-rw-r--r--collections/boards.js251
1 files changed, 251 insertions, 0 deletions
diff --git a/collections/boards.js b/collections/boards.js
new file mode 100644
index 00000000..e406b10c
--- /dev/null
+++ b/collections/boards.js
@@ -0,0 +1,251 @@
+Boards = new Mongo.Collection('boards');
+
+Boards.attachSchema(new SimpleSchema({
+ title: {
+ type: String
+ },
+ slug: {
+ type: String
+ },
+ archived: {
+ type: Boolean
+ },
+ createdAt: {
+ type: Date,
+ denyUpdate: true
+ },
+ // XXX Inconsistent field naming
+ modifiedAt: {
+ type: Date,
+ denyInsert: true,
+ optional: true
+ },
+ // De-normalized number of users that have starred this board
+ stars: {
+ type: Number
+ },
+ // De-normalized label system
+ 'labels.$._id': {
+ // We don't specify that this field must be unique in the board because that
+ // will cause performance penalties and is not necessary since this field is
+ // always set on the server.
+ // XXX Actually if we create a new label, the `_id` is set on the client
+ // without being overwritten by the server, could it be a problem?
+ type: String
+ },
+ 'labels.$.name': {
+ type: String,
+ optional: true
+ },
+ 'labels.$.color': {
+ type: String,
+ allowedValues: [
+ 'green', 'yellow', 'orange', 'red', 'purple',
+ 'blue', 'sky', 'lime', 'pink', 'black'
+ ]
+ },
+ // XXX We might want to maintain more informations under the member sub-
+ // documents like de-normalized meta-data (the date the member joined the
+ // board, the number of contributions, etc.).
+ 'members.$.userId': {
+ type: String
+ },
+ 'members.$.isAdmin': {
+ type: Boolean
+ },
+ 'members.$.isActive': {
+ type: Boolean
+ },
+ permission: {
+ type: String,
+ allowedValues: ['public', 'private']
+ },
+ color: {
+ type: String,
+ allowedValues: ['nephritis', 'pomegranate', 'belize',
+ 'wisteria', 'midnight', 'pumpkin']
+ }
+}));
+
+if (Meteor.isServer) {
+ Boards.allow({
+ insert: Meteor.userId,
+ update: allowIsBoardAdmin,
+ remove: allowIsBoardAdmin,
+ fetch: ['members']
+ });
+
+ // The number of users that have starred this board is managed by trusted code
+ // and the user is not allowed to update it
+ Boards.deny({
+ update: function(userId, board, fieldNames) {
+ return _.contains(fieldNames, 'stars');
+ },
+ fetch: []
+ });
+
+ // We can't remove a member if it is the last administrator
+ Boards.deny({
+ update: function(userId, doc, fieldNames, modifier) {
+ if (! _.contains(fieldNames, 'members'))
+ return false;
+
+ // We only care in case of a $pull operation, ie remove a member
+ if (! _.isObject(modifier.$pull && modifier.$pull.members))
+ return false;
+
+ // If there is more than one admin, it's ok to remove anyone
+ var nbAdmins = _.filter(doc.members, function(member) {
+ return member.isAdmin;
+ }).length;
+ if (nbAdmins > 1)
+ return false;
+
+ // If all the previous conditions where verified, we can't remove
+ // a user if it's an admin
+ var removedMemberId = modifier.$pull.members.userId;
+ return !! _.findWhere(doc.members, {
+ userId: removedMemberId,
+ isAdmin: true
+ });
+ },
+ fetch: ['members']
+ });
+}
+
+Boards.helpers({
+ isPublic: function() {
+ return this.permission === 'public';
+ },
+ lists: function() {
+ return Lists.find({ boardId: this._id, archived: false },
+ { sort: { sort: 1 }});
+ },
+ activities: function() {
+ return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }});
+ },
+ absoluteUrl: function() {
+ return Router.path('Board', { boardId: this._id, slug: this.slug });
+ },
+ colorClass: function() {
+ return 'board-color-' + this.color;
+ }
+});
+
+Boards.before.insert(function(userId, doc) {
+ // XXX We need to improve slug management. Only the id should be necessary
+ // to identify a board in the code.
+ // XXX If the board title is updated, the slug should also be updated.
+ // In some cases (Chinese and Japanese for instance) the `getSlug` function
+ // return an empty string. This is causes bugs in our application so we set
+ // a default slug in this case.
+ doc.slug = getSlug(doc.title) || 'board';
+ doc.createdAt = new Date();
+ doc.archived = false;
+ doc.members = [{
+ userId: userId,
+ isAdmin: true,
+ isActive: true
+ }];
+ doc.stars = 0;
+ doc.color = Boards.simpleSchema()._schema.color.allowedValues[0];
+
+ // Handle labels
+ var colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
+ var defaultLabelsColors = _.clone(colors).splice(0, 6);
+ doc.labels = [];
+ _.each(defaultLabelsColors, function(val) {
+ doc.labels.push({
+ _id: Random.id(6),
+ name: '',
+ color: val
+ });
+ });
+
+ // We randomly chose one of the default background colors for the board
+ if (Meteor.isClient) {
+ doc.background = {
+ type: 'color',
+ color: Random.choice(Boards.simpleSchema()._schema.color.allowedValues)
+ };
+ }
+});
+
+Boards.before.update(function(userId, doc, fieldNames, modifier) {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = new Date();
+});
+
+if (Meteor.isServer) {
+ // Let MongoDB ensure that a member is not included twice in the same board
+ Meteor.startup(function() {
+ Boards._collection._ensureIndex({
+ _id: 1,
+ 'members.userId': 1
+ }, { unique: true });
+ });
+
+ // Genesis: the first activity of the newly created board
+ Boards.after.insert(function(userId, doc) {
+ Activities.insert({
+ type: 'board',
+ activityTypeId: doc._id,
+ activityType: 'createBoard',
+ boardId: doc._id,
+ userId: userId
+ });
+ });
+
+ // If the user remove one label from a board, we cant to remove reference of
+ // this label in any card of this board.
+ Boards.after.update(function(userId, doc, fieldNames, modifier) {
+ if (! _.contains(fieldNames, 'labels') ||
+ ! modifier.$pull ||
+ ! modifier.$pull.labels ||
+ ! modifier.$pull.labels._id)
+ return;
+
+ var removedLabelId = modifier.$pull.labels._id;
+ Cards.update(
+ { boardId: doc._id },
+ {
+ $pull: {
+ labels: removedLabelId
+ }
+ },
+ { multi: true }
+ );
+ });
+
+ // Add a new activity if we add or remove a member to the board
+ Boards.after.update(function(userId, doc, fieldNames, modifier) {
+ if (! _.contains(fieldNames, 'members'))
+ return;
+
+ var memberId;
+
+ // Say hello to the new member
+ if (modifier.$push && modifier.$push.members) {
+ memberId = modifier.$push.members.userId;
+ Activities.insert({
+ type: 'member',
+ activityType: 'addBoardMember',
+ boardId: doc._id,
+ userId: userId,
+ memberId: memberId
+ });
+ }
+
+ // Say goodbye to the former member
+ if (modifier.$pull && modifier.$pull.members) {
+ memberId = modifier.$pull.members.userId;
+ Activities.insert({
+ type: 'member',
+ activityType: 'removeBoardMember',
+ boardId: doc._id,
+ userId: userId,
+ memberId: memberId
+ });
+ }
+ });
+}