summaryrefslogtreecommitdiffstats
path: root/models/boards.js
diff options
context:
space:
mode:
Diffstat (limited to 'models/boards.js')
-rw-r--r--models/boards.js348
1 files changed, 348 insertions, 0 deletions
diff --git a/models/boards.js b/models/boards.js
new file mode 100644
index 00000000..4baec280
--- /dev/null
+++ b/models/boards.js
@@ -0,0 +1,348 @@
+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: [
+ 'belize',
+ 'nephritis',
+ 'pomegranate',
+ 'pumpkin',
+ 'wisteria',
+ 'midnight',
+ ],
+ },
+}));
+
+
+Boards.helpers({
+ isPublic() {
+ return this.permission === 'public';
+ },
+
+ lists() {
+ return Lists.find({ boardId: this._id, archived: false },
+ { sort: { sort: 1 }});
+ },
+
+ activities() {
+ return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }});
+ },
+
+ activeMembers() {
+ return _.where(this.members, {isActive: true});
+ },
+
+ labelIndex(labelId) {
+ return _.indexOf(_.pluck(this.labels, '_id'), labelId);
+ },
+
+ memberIndex(memberId) {
+ return _.indexOf(_.pluck(this.members, 'userId'), memberId);
+ },
+
+ absoluteUrl() {
+ return FlowRouter.path('board', { id: this._id, slug: this.slug });
+ },
+
+ colorClass() {
+ return `board-color-${this.color}`;
+ },
+});
+
+Boards.mutations({
+ archive() {
+ return { $set: { archived: true }};
+ },
+
+ restore() {
+ return { $set: { archived: false }};
+ },
+
+ rename(title) {
+ return { $set: { title }};
+ },
+
+ setColor(color) {
+ return { $set: { color }};
+ },
+
+ setVisibility(visibility) {
+ return { $set: { permission: visibility }};
+ },
+
+ addLabel(name, color) {
+ const _id = Random.id(6);
+ return { $push: {labels: { _id, name, color }}};
+ },
+
+ editLabel(labelId, name, color) {
+ const labelIndex = this.labelIndex(labelId);
+ return {
+ $set: {
+ [`labels.${labelIndex}.name`]: name,
+ [`labels.${labelIndex}.color`]: color,
+ },
+ };
+ },
+
+ removeLabel(labelId) {
+ return { $pull: { labels: { _id: labelId }}};
+ },
+
+ addMember(memberId) {
+ const memberIndex = this.memberIndex(memberId);
+ if (memberIndex === -1) {
+ return {
+ $push: {
+ members: {
+ userId: memberId,
+ isAdmin: false,
+ isActive: true,
+ },
+ },
+ };
+ } else {
+ return {
+ $set: {
+ [`members.${memberIndex}.isActive`]: true,
+ [`members.${memberIndex}.isAdmin`]: false,
+ },
+ };
+ }
+ },
+
+ removeMember(memberId) {
+ const memberIndex = this.memberIndex(memberId);
+
+ return {
+ $set: {
+ [`members.${memberIndex}.isActive`]: false,
+ },
+ };
+ },
+
+ setMemberPermission(memberId, isAdmin) {
+ const memberIndex = this.memberIndex(memberId);
+
+ return {
+ $set: {
+ [`members.${memberIndex}.isAdmin`]: isAdmin,
+ },
+ };
+ },
+});
+
+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(userId, board, fieldNames) {
+ return _.contains(fieldNames, 'stars');
+ },
+ fetch: [],
+ });
+
+ // We can't remove a member if it is the last administrator
+ Boards.deny({
+ update(userId, doc, fieldNames, modifier) {
+ if (!_.contains(fieldNames, 'members'))
+ return false;
+
+ // 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
+ const nbAdmins = _.filter(doc.members, (member) => {
+ return member.isAdmin;
+ }).length;
+ if (nbAdmins > 1)
+ return false;
+
+ // If all the previous conditions were verified, we can't remove
+ // a user if it's an admin
+ const removedMemberId = modifier.$pull.members.userId;
+ return Boolean(_.findWhere(doc.members, {
+ userId: removedMemberId,
+ isAdmin: true,
+ }));
+ },
+ fetch: ['members'],
+ });
+}
+
+Boards.before.insert((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 = doc.slug || getSlug(doc.title) || 'board';
+ doc.createdAt = new Date();
+ doc.archived = false;
+ doc.members = doc.members || [{
+ userId,
+ isAdmin: true,
+ isActive: true,
+ }];
+ doc.stars = 0;
+ doc.color = Boards.simpleSchema()._schema.color.allowedValues[0];
+
+ // Handle labels
+ const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
+ const defaultLabelsColors = _.clone(colors).splice(0, 6);
+ doc.labels = _.map(defaultLabelsColors, (color) => {
+ return {
+ color,
+ _id: Random.id(6),
+ name: '',
+ };
+ });
+});
+
+Boards.before.update((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(() => {
+ Boards._collection._ensureIndex({
+ _id: 1,
+ 'members.userId': 1,
+ }, { unique: true });
+ });
+
+ // Genesis: the first activity of the newly created board
+ Boards.after.insert((userId, doc) => {
+ Activities.insert({
+ userId,
+ type: 'board',
+ activityTypeId: doc._id,
+ activityType: 'createBoard',
+ boardId: doc._id,
+ });
+ });
+
+ // If the user remove one label from a board, we cant to remove reference of
+ // this label in any card of this board.
+ Boards.after.update((userId, doc, fieldNames, modifier) => {
+ if (!_.contains(fieldNames, 'labels') ||
+ !modifier.$pull ||
+ !modifier.$pull.labels ||
+ !modifier.$pull.labels._id)
+ return;
+
+ const 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((userId, doc, fieldNames, modifier) => {
+ if (!_.contains(fieldNames, 'members'))
+ return;
+
+ let memberId;
+
+ // Say hello to the new member
+ if (modifier.$push && modifier.$push.members) {
+ memberId = modifier.$push.members.userId;
+ Activities.insert({
+ userId,
+ memberId,
+ type: 'member',
+ activityType: 'addBoardMember',
+ boardId: doc._id,
+ });
+ }
+
+ // Say goodbye to the former member
+ if (modifier.$pull && modifier.$pull.members) {
+ memberId = modifier.$pull.members.userId;
+ Activities.insert({
+ userId,
+ memberId,
+ type: 'member',
+ activityType: 'removeBoardMember',
+ boardId: doc._id,
+ });
+ }
+ });
+}