text: Boards = new Mongo.Collection('boards');
  
  Boards.attachSchema(new SimpleSchema({
    title: {
      type: String,
    },
    slug: {
      type: String,
      autoValue() { // eslint-disable-line consistent-return
        // 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.
        if (this.isInsert && !this.isSet) {
          let slug = 'board';
          const title = this.field('title');
          if (title.isSet) {
            slug = getSlug(title.value) || slug;
          }
          return slug;
        }
      },
    },
    archived: {
      type: Boolean,
      autoValue() { // eslint-disable-line consistent-return
        if (this.isInsert && !this.isSet) {
          return false;
        }
      },
    },
    createdAt: {
      type: Date,
      autoValue() { // eslint-disable-line consistent-return
        if (this.isInsert) {
          return new Date();
        } else {
          this.unset();
        }
      },
    },
    // XXX Inconsistent field naming
    modifiedAt: {
      type: Date,
      optional: true,
      autoValue() { // eslint-disable-line consistent-return
        if (this.isUpdate) {
          return new Date();
        } else {
          this.unset();
        }
      },
    },
    // De-normalized number of users that have starred this board
    stars: {
      type: Number,
      autoValue() { // eslint-disable-line consistent-return
        if (this.isInsert) {
          return 0;
        }
      },
    },
    // De-normalized label system
    'labels': {
      type: [Object],
      autoValue() { // eslint-disable-line consistent-return
        if (this.isInsert && !this.isSet) {
          const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
          const defaultLabelsColors = _.clone(colors).splice(0, 6);
          return defaultLabelsColors.map((color) => ({
            color,
            _id: Random.id(6),
            name: '',
          }));
        }
      },
    },
    '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',
        '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-
    // documents like de-normalized meta-data (the date the member joined the
    // board, the number of contributions, etc.).
    'members': {
      type: [Object],
      autoValue() { // eslint-disable-line consistent-return
        if (this.isInsert && !this.isSet) {
          return [{
            userId: this.userId,
            isAdmin: true,
            isActive: true,
            isNoComments: false,
            isCommentOnly: false,
          }];
        }
      },
    },
    'members.$.userId': {
      type: String,
    },
    'members.$.isAdmin': {
      type: Boolean,
    },
    'members.$.isActive': {
      type: Boolean,
    },
    'members.$.isNoComments': {
      type: Boolean,
    },
    'members.$.isCommentOnly': {
      type: Boolean,
    },
    permission: {
      type: String,
      allowedValues: ['public', 'private'],
    },
    color: {
      type: String,
      allowedValues: [
        'belize',
        'nephritis',
        'pomegranate',
        'pumpkin',
        'wisteria',
        'midnight',
      ],
      autoValue() { // eslint-disable-line consistent-return
        if (this.isInsert && !this.isSet) {
          return Boards.simpleSchema()._schema.color.allowedValues[0];
        }
      },
    },
    description: {
      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,
    },
  }));
  
  
  Boards.helpers({
    /**
     * Is supplied user authorized to view this board?
     */
    isVisibleBy(user) {
      if (this.isPublic()) {
        // public boards are visible to everyone
        return true;
      } else {
        // otherwise you have to be logged-in and active member
        return user && this.isActiveMember(user._id);
      }
    },
  
    /**
     * Is the user one of the active members of the board?
     *
     * @param userId
     * @returns {boolean} the member that matches, or undefined/false
     */
    isActiveMember(userId) {
      if (userId) {
        return this.members.find((member) => (member.userId === userId && member.isActive));
      } else {
        return false;
      }
    },
  
    isPublic() {
      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 } });
    },
  
    swimlanes() {
      return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
    },
  
    hasOvertimeCards(){
      const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
      return card !== undefined;
    },
  
    hasSpentTimeCards(){
      const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} );
      return card !== undefined;
    },
  
    activities() {
      return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 } });
    },
  
    activeMembers() {
      return _.where(this.members, { isActive: true });
    },
  
    activeAdmins() {
      return _.where(this.members, { isActive: true, isAdmin: true });
    },
  
    memberUsers() {
      return Users.find({ _id: { $in: _.pluck(this.members, 'userId') } });
    },
  
    getLabel(name, color) {
      return _.findWhere(this.labels, { name, color });
    },
  
    labelIndex(labelId) {
      return _.pluck(this.labels, '_id').indexOf(labelId);
    },
  
    memberIndex(memberId) {
      return _.pluck(this.members, 'userId').indexOf(memberId);
    },
  
    hasMember(memberId) {
      return !!_.findWhere(this.members, { userId: memberId, isActive: true });
    },
  
    hasAdmin(memberId) {
      return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true });
    },
  
    hasNoComments(memberId) {
      return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isNoComments: true });
    },
  
    hasCommentOnly(memberId) {
      return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isCommentOnly: true });
    },
  
    absoluteUrl() {
      return FlowRouter.url('board', { id: this._id, slug: this.slug });
    },
  
    colorClass() {
      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) {
      const _id = Random.id(6);
      Boards.direct.update(this._id, { $push: { labels: { _id, name, color } } });
      return _id;
    },
  
    searchCards(term, excludeLinked) {
      check(term, Match.OneOf(String, null, undefined));
  
      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.$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 } };
    },
  
    restore() {
      return { $set: { archived: false } };
    },
  
    rename(title) {
      return { $set: { title } };
    },
  
    setDescription(description) {
      return { $set: { description } };
    },
  
    setColor(color) {
      return { $set: { color } };
    },
  
    setVisibility(visibility) {
      return { $set: { permission: visibility } };
    },
  
    addLabel(name, color) {
      // If label with the same name and color already exists we don't want to
      // create another one because they would be indistinguishable in the UI
      // (they would still have different `_id` but that is not exposed to the
      // user).
      if (!this.getLabel(name, color)) {
        const _id = Random.id(6);
        return { $push: { labels: { _id, name, color } } };
      }
      return {};
    },
  
    editLabel(labelId, name, color) {
      if (!this.getLabel(name, color)) {
        const labelIndex = this.labelIndex(labelId);
        return {
          $set: {
            [`labels.${labelIndex}.name`]: name,
            [`labels.${labelIndex}.color`]: color,
          },
        };
      }
      return {};
    },
  
    removeLabel(labelId) {
      return { $pull: { labels: { _id: labelId } } };
    },
  
    changeOwnership(fromId, toId) {
      const memberIndex = this.memberIndex(fromId);
      return {
        $set: {
          [`members.${memberIndex}.userId`]: toId,
        },
      };
    },
  
    addMember(memberId) {
      const memberIndex = this.memberIndex(memberId);
      if (memberIndex >= 0) {
        return {
          $set: {
            [`members.${memberIndex}.isActive`]: true,
          },
        };
      }
  
      return {
        $push: {
          members: {
            userId: memberId,
            isAdmin: false,
            isActive: true,
            isNoComments: false,
            isCommentOnly: false,
          },
        },
      };
    },
  
    removeMember(memberId) {
      const memberIndex = this.memberIndex(memberId);
  
      // we do not allow the only one admin to be removed
      const allowRemove = (!this.members[memberIndex].isAdmin) || (this.activeAdmins().length > 1);
      if (!allowRemove) {
        return {
          $set: {
            [`members.${memberIndex}.isActive`]: true,
          },
        };
      }
  
      return {
        $set: {
          [`members.${memberIndex}.isActive`]: false,
          [`members.${memberIndex}.isAdmin`]: false,
        },
      };
    },
  
    setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly) {
      const memberIndex = this.memberIndex(memberId);
  
      // do not allow change permission of self
      if (memberId === Meteor.userId()) {
        isAdmin = this.members[memberIndex].isAdmin;
      }
  
      return {
        $set: {
          [`members.${memberIndex}.isAdmin`]: isAdmin,
          [`members.${memberIndex}.isNoComments`]: isNoComments,
          [`members.${memberIndex}.isCommentOnly`]: isCommentOnly,
        },
      };
    },
  
    setAllowsSubtasks(allowsSubtasks) {
      return { $set: { allowsSubtasks } };
    },
  
    setSubtasksDefaultBoardId(subtasksDefaultBoardId) {
      return { $set: { subtasksDefaultBoardId } };
    },
  
    setSubtasksDefaultListId(subtasksDefaultListId) {
      return { $set: { subtasksDefaultListId } };
    },
  
    setPresentParentTask(presentParentTask) {
      return { $set: { presentParentTask } };
    },
  });
  
  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 = _.where(doc.members, { isActive: true, isAdmin: true }).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'],
    });
  
    Meteor.methods({
      quitBoard(boardId) {
        check(boardId, String);
        const board = Boards.findOne(boardId);
        if (board) {
          const userId = Meteor.userId();
          const index = board.memberIndex(userId);
          if (index >= 0) {
            board.removeMember(userId);
            return true;
          } else throw new Meteor.Error('error-board-notAMember');
        } else throw new Meteor.Error('error-board-doesNotExist');
      },
    });
  }
  
  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 });
      Boards._collection._ensureIndex({ 'members.userId': 1 });
    });
  
    // 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: {
            labelIds: removedLabelId,
          },
        },
        { multi: true }
      );
    });
  
    const foreachRemovedMember = (doc, modifier, callback) => {
      Object.keys(modifier).forEach((set) => {
        if (modifier[set] !== false) {
          return;
        }
  
        const parts = set.split('.');
        if (parts.length === 3 && parts[0] === 'members' && parts[2] === 'isActive') {
          callback(doc.members[parts[1]].userId);
        }
      });
    };
  
    // Remove a member from all objects of the board before leaving the board
    Boards.before.update((userId, doc, fieldNames, modifier) => {
      if (!_.contains(fieldNames, 'members')) {
        return;
      }
  
      if (modifier.$set) {
        const boardId = doc._id;
        foreachRemovedMember(doc, modifier.$set, (memberId) => {
          Cards.update(
            { boardId },
            {
              $pull: {
                members: memberId,
                watchers: memberId,
              },
            },
            { multi: true }
          );
  
          Lists.update(
            { boardId },
            {
              $pull: {
                watchers: memberId,
              },
            },
            { multi: true }
          );
  
          const board = Boards._transform(doc);
          board.setWatcher(memberId, false);
  
          // Remove board from users starred list
          if (!board.isPublic()) {
            Users.update(
              memberId,
              {
                $pull: {
                  'profile.starredBoards': boardId,
                },
              }
            );
          }
        });
      }
    });
  
    // 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;
      }
  
      // Say hello to the new member
      if (modifier.$push && modifier.$push.members) {
        const memberId = modifier.$push.members.userId;
        Activities.insert({
          userId,
          memberId,
          type: 'member',
          activityType: 'addBoardMember',
          boardId: doc._id,
        });
      }
  
      // Say goodbye to the former member
      if (modifier.$set) {
        foreachRemovedMember(doc, modifier.$set, (memberId) => {
          Activities.insert({
            userId,
            memberId,
            type: 'member',
            activityType: 'removeBoardMember',
            boardId: doc._id,
          });
        });
      }
    });
  }
  
  //BOARDS REST API
  if (Meteor.isServer) {
    JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) {
      try {
        Authentication.checkLoggedIn(req.userId);
        const paramUserId = req.params.userId;
        // A normal user should be able to see their own boards,
        // admins can access boards of any user
        Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId);
  
        const data = Boards.find({
          archived: false,
          'members.userId': paramUserId,
        }, {
          sort: ['title'],
        }).map(function(board) {
          return {
            _id: board._id,
            title: board.title,
          };
        });
  
        JsonRoutes.sendResult(res, {code: 200, data});
      }
      catch (error) {
        JsonRoutes.sendResult(res, {
          code: 200,
          data: error,
        });
      }
    });
  
    JsonRoutes.add('GET', '/api/boards', function (req, res) {
      try {
        Authentication.checkUserId(req.userId);
        JsonRoutes.sendResult(res, {
          code: 200,
          data: Boards.find({ permission: 'public' }).map(function (doc) {
            return {
              _id: doc._id,
              title: doc.title,
            };
          }),
        });
      }
      catch (error) {
        JsonRoutes.sendResult(res, {
          code: 200,
          data: error,
        });
      }
    });
  
    JsonRoutes.add('GET', '/api/boards/:id', function (req, res) {
      try {
        const id = req.params.id;
        Authentication.checkBoardAccess(req.userId, id);
  
        JsonRoutes.sendResult(res, {
          code: 200,
          data: Boards.findOne({ _id: id }),
        });
      }
      catch (error) {
        JsonRoutes.sendResult(res, {
          code: 200,
          data: error,
        });
      }
    });
  
    JsonRoutes.add('POST', '/api/boards', function (req, res) {
      try {
        Authentication.checkUserId(req.userId);
        const id = Boards.insert({
          title: req.body.title,
          members: [
            {
              userId: req.body.owner,
              isAdmin: req.body.isAdmin || true,
              isActive: req.body.isActive || true,
              isNoComments: req.body.isNoComments || false,
              isCommentOnly: req.body.isCommentOnly || false,
            },
          ],
          permission: req.body.permission || 'private',
          color: req.body.color || 'belize',
        });
        const swimlaneId = Swimlanes.insert({
          title: TAPi18n.__('default'),
          boardId: id,
        });
        JsonRoutes.sendResult(res, {
          code: 200,
          data: {
            _id: id,
            defaultSwimlaneId: swimlaneId,
          },
        });
      }
      catch (error) {
        JsonRoutes.sendResult(res, {
          code: 200,
          data: error,
        });
      }
    });
  
    JsonRoutes.add('DELETE', '/api/boards/:id', function (req, res) {
      try {
        Authentication.checkUserId(req.userId);
        const id = req.params.id;
        Boards.remove({ _id: id });
        JsonRoutes.sendResult(res, {
          code: 200,
          data:{
            _id: id,
          },
        });
      }
      catch (error) {
        JsonRoutes.sendResult(res, {
          code: 200,
          data: error,
        });
      }
    });
  
    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,
        });
      }
    });
  }
  