<commit_before>// Activities don't need a schema because they are always set from the a trusted
// environment - the server - and there is no risk that a user change the logic
// we use with this collection. Moreover using a schema for this collection
// would be difficult (different activities have different fields) and wouldn't
// bring any direct advantage.
//
// XXX The activities API is not so nice and need some functionalities. For
// instance if a user archive a card, and un-archive it a few seconds later we
// should remove both activities assuming it was an error the user decided to
// revert.
Activities = new Mongo.Collection('activities');

Activities.helpers({
  board() {
    return Boards.findOne(this.boardId);
  },
  oldBoard() {
    return Boards.findOne(this.oldBoardId);
  },
  user() {
    return Users.findOne(this.userId);
  },
  member() {
    return Users.findOne(this.memberId);
  },
  list() {
    return Lists.findOne(this.listId);
  },
  swimlane() {
    return Swimlanes.findOne(this.swimlaneId);
  },
  oldSwimlane() {
    return Swimlanes.findOne(this.oldSwimlaneId);
  },
  oldList() {
    return Lists.findOne(this.oldListId);
  },
  card() {
    return Cards.findOne(this.cardId);
  },
  comment() {
    return CardComments.findOne(this.commentId);
  },
  attachment() {
    return Attachments.findOne(this.attachmentId);
  },
  checklist() {
    return Checklists.findOne(this.checklistId);
  },
  checklistItem() {
    return ChecklistItems.findOne(this.checklistItemId);
  },
  subtasks() {
    return Cards.findOne(this.subtaskId);
  },
  customField() {
    return CustomFields.findOne(this.customFieldId);
  },
  // Label activity did not work yet, unable to edit labels when tried this.
  //label() {
  //  return Cards.findOne(this.labelId);
  //},
});

Activities.before.update((userId, doc, fieldNames, modifier) => {
  modifier.$set = modifier.$set || {};
  modifier.$set.modifiedAt = new Date();
});

Activities.before.insert((userId, doc) => {
  doc.createdAt = new Date();
  doc.modifiedAt = doc.createdAt;
});

Activities.after.insert((userId, doc) => {
  const activity = Activities._transform(doc);
  RulesHelper.executeRules(activity);
});

if (Meteor.isServer) {
  // For efficiency create indexes on the date of creation, and on the date of
  // creation in conjunction with the card or board id, as corresponding views
  // are largely used in the App. See #524.
  Meteor.startup(() => {
    Activities._collection._ensureIndex({ createdAt: -1 });
    Activities._collection._ensureIndex({ modifiedAt: -1 });
    Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 });
    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 } } },
    );
    // Label activity did not work yet, unable to edit labels when tried this.
    //Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 });
    //Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
  });

  Activities.after.insert((userId, doc) => {
    const activity = Activities._transform(doc);
    let participants = [];
    let watchers = [];
    let title = 'act-activity-notify';
    const board = Boards.findOne(activity.boardId);
    const description = `act-${activity.activityType}`;
    const params = {
      activityId: activity._id,
    };
    if (activity.userId) {
      // No need send notification to user of activity
      // participants = _.union(participants, [activity.userId]);
      const user = activity.user();
      if (user) {
        if (user.getName()) {
          params.user = user.getName();
        }
        if (user.emails) {
          params.userEmails = user.emails;
        }
        if (activity.userId) {
          params.userId = activity.userId;
        }
      }
    }
    if (activity.boardId) {
      if (board.title.length > 0) {
        params.board = board.title;
      } else {
        params.board = '';
      }
      title = 'act-withBoardTitle';
      params.url = board.absoluteUrl();
      params.boardId = activity.boardId;
    }
    if (activity.oldBoardId) {
      const oldBoard = activity.oldBoard();
      if (oldBoard) {
        watchers = _.union(watchers, oldBoard.watchers || []);
        params.oldBoard = oldBoard.title;
        params.oldBoardId = activity.oldBoardId;
      }
    }
    if (activity.memberId) {
      participants = _.union(participants, [activity.memberId]);
      params.member = activity.member().getName();
    }
    if (activity.listId) {
      const list = activity.list();
      watchers = _.union(watchers, list.watchers || []);
      params.list = list.title;
      params.listId = activity.listId;
    }
    if (activity.oldListId) {
      const oldList = activity.oldList();
      if (oldList) {
        watchers = _.union(watchers, oldList.watchers || []);
        params.oldList = oldList.title;
        params.oldListId = activity.oldListId;
      }
    }
    if (activity.oldSwimlaneId) {
      const oldSwimlane = activity.oldSwimlane();
      if (oldSwimlane) {
        watchers = _.union(watchers, oldSwimlane.watchers || []);
        params.oldSwimlane = oldSwimlane.title;
        params.oldSwimlaneId = activity.oldSwimlaneId;
      }
    }
    if (activity.cardId) {
      const card = activity.card();
      participants = _.union(participants, [card.userId], card.members || []);
      watchers = _.union(watchers, card.watchers || []);
      params.card = card.title;
      title = 'act-withCardTitle';
      params.url = card.absoluteUrl();
      params.cardId = activity.cardId;
    }
    if (activity.swimlaneId) {
      const swimlane = activity.swimlane();
      params.swimlane = swimlane.title;
      params.swimlaneId = activity.swimlaneId;
    }
    if (activity.commentId) {
      const comment = activity.comment();
      params.comment = comment.text;
      if (board) {
        const comment = params.comment;
        const knownUsers = board.members.map(member => {
          const u = Users.findOne(member.userId);
          if (u) {
            member.username = u.username;
            member.emails = u.emails;
          }
          return member;
        });
        const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username
        let currentMention;
        while ((currentMention = mentionRegex.exec(comment)) !== null) {
          /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[iI]gnored" }]*/
          const [ignored, quoteduser, simple] = currentMention;
          const username = quoteduser || simple;
          if (username === params.user) {
            // ignore commenter mention himself?
            continue;
          }

          if (activity.boardId && username === 'board_members') {
            // mentions all board members
            const knownUids = knownUsers.map(u => u.userId);
            watchers = _.union(watchers, [...knownUids]);
            title = 'act-atUserComment';
          } else if (activity.cardId && username === 'card_members') {
            // mentions all card members if assigned
            const card = activity.card();
            watchers = _.union(watchers, [...card.members]);
            title = 'act-atUserComment';
          } else {
            const atUser = _.findWhere(knownUsers, { username });
            if (!atUser) {
              continue;
            }

            const uid = atUser.userId;
            params.atUsername = username;
            params.atEmails = atUser.emails;
            title = 'act-atUserComment';
            watchers = _.union(watchers, [uid]);
          }

        }
      }
      params.commentId = comment._id;
    }
    if (activity.attachmentId) {
      const attachment = activity.attachment();
      params.attachment = attachment.original.name;
      params.attachmentId = attachment._id;
    }
    if (activity.checklistId) {
      const checklist = activity.checklist();
      if (checklist) {
        if (checklist.title) {
          params.checklist = checklist.title;
        }
      }
    }
    if (activity.checklistItemId) {
      const checklistItem = activity.checklistItem();
      if (checklistItem) {
        if (checklistItem.title) {
          params.checklistItem = checklistItem.title;
        }
      }
    }
    if (activity.customFieldId) {
      const customField = activity.customField();
      if (customField) {
        if (customField.name) {
          params.customField = customField.name;
        }
        if (activity.value) {
          params.customFieldValue = activity.value;
        }
      }
    }
    // Label activity did not work yet, unable to edit labels when tried this.
    //if (activity.labelId) {
    //  const label = activity.label();
    //  params.label = label.name;
    //  params.labelId = activity.labelId;
    //}
    if (
      (!activity.timeKey || activity.timeKey === 'dueAt') &&
      activity.timeValue
    ) {
      // due time reminder, if it doesn't have old value, it's a brand new set, need some differentiation
      title = activity.timeOldValue ? 'act-withDue' : 'act-newDue';
    }
    ['timeValue', 'timeOldValue'].forEach(key => {
      // copy time related keys & values to params
      const value = activity[key];
      if (value) params[key] = value;
    });
    if (board) {
      const BIGEVENTS = process.env.BIGEVENTS_PATTERN; // if environment BIGEVENTS_PATTERN is set, any activityType matching it is important
      if (BIGEVENTS) {
        try {
          const atype = activity.activityType;
          if (new RegExp(BIGEVENTS).exec(atype)) {
            watchers = _.union(
              watchers,
              board.activeMembers().map(member => member.userId),
            ); // notify all active members for important events
          }
        } catch (e) {
          // passed env var BIGEVENTS_PATTERN is not a valid regex
        }
      }

      const watchingUsers = _.pluck(
        _.where(board.watchers, { level: 'watching' }),
        'userId',
      );
      const trackingUsers = _.pluck(
        _.where(board.watchers, { level: 'tracking' }),
        'userId',
      );
      watchers = _.union(
        watchers,
        watchingUsers,
        _.intersection(participants, trackingUsers),
      );
    }
    Notifications.getUsers(watchers).forEach(user => {
      // don't notify a user of their own behavior
      if (user._id !== userId) {
        Notifications.notify(user, title, description, params);
      }
    });

    const integrations = Integrations.find({
      boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] },
      // type: 'outgoing-webhooks', // all types
      enabled: true,
      activities: { $in: [description, 'all'] },
    }).fetch();
    if (integrations.length > 0) {
      params.watchers = watchers;
      integrations.forEach(integration => {
        Meteor.call(
          'outgoingWebhooks',
          integration,
          description,
          params,
          () => {
            return;
          },
        );
      });
    }
  });
}

export default Activities;
<commit_msg>Try to fix: Can't delete attachment.<commit_after>// Activities don't need a schema because they are always set from the a trusted
// environment - the server - and there is no risk that a user change the logic
// we use with this collection. Moreover using a schema for this collection
// would be difficult (different activities have different fields) and wouldn't
// bring any direct advantage.
//
// XXX The activities API is not so nice and need some functionalities. For
// instance if a user archive a card, and un-archive it a few seconds later we
// should remove both activities assuming it was an error the user decided to
// revert.
Activities = new Mongo.Collection('activities');

Activities.helpers({
  board() {
    return Boards.findOne(this.boardId);
  },
  oldBoard() {
    return Boards.findOne(this.oldBoardId);
  },
  user() {
    return Users.findOne(this.userId);
  },
  member() {
    return Users.findOne(this.memberId);
  },
  list() {
    return Lists.findOne(this.listId);
  },
  swimlane() {
    return Swimlanes.findOne(this.swimlaneId);
  },
  oldSwimlane() {
    return Swimlanes.findOne(this.oldSwimlaneId);
  },
  oldList() {
    return Lists.findOne(this.oldListId);
  },
  card() {
    return Cards.findOne(this.cardId);
  },
  comment() {
    return CardComments.findOne(this.commentId);
  },
  attachment() {
    return Attachments.findOne(this.attachmentId);
  },
  checklist() {
    return Checklists.findOne(this.checklistId);
  },
  checklistItem() {
    return ChecklistItems.findOne(this.checklistItemId);
  },
  subtasks() {
    return Cards.findOne(this.subtaskId);
  },
  customField() {
    return CustomFields.findOne(this.customFieldId);
  },
  // Label activity did not work yet, unable to edit labels when tried this.
  //label() {
  //  return Cards.findOne(this.labelId);
  //},
});

Activities.before.update((userId, doc, fieldNames, modifier) => {
  modifier.$set = modifier.$set || {};
  modifier.$set.modifiedAt = new Date();
});

Activities.before.insert((userId, doc) => {
  doc.createdAt = new Date();
  doc.modifiedAt = doc.createdAt;
});

Activities.after.insert((userId, doc) => {
  const activity = Activities._transform(doc);
  RulesHelper.executeRules(activity);
});

if (Meteor.isServer) {
  // For efficiency create indexes on the date of creation, and on the date of
  // creation in conjunction with the card or board id, as corresponding views
  // are largely used in the App. See #524.
  Meteor.startup(() => {
    Activities._collection._ensureIndex({ createdAt: -1 });
    Activities._collection._ensureIndex({ modifiedAt: -1 });
    Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 });
    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 } } },
    );
    // Label activity did not work yet, unable to edit labels when tried this.
    //Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 });
    //Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
  });

  Activities.after.insert((userId, doc) => {
    const activity = Activities._transform(doc);
    let participants = [];
    let watchers = [];
    let title = 'act-activity-notify';
    const board = Boards.findOne(activity.boardId);
    const description = `act-${activity.activityType}`;
    const params = {
      activityId: activity._id,
    };
    if (activity.userId) {
      // No need send notification to user of activity
      // participants = _.union(participants, [activity.userId]);
      const user = activity.user();
      if (user) {
        if (user.getName()) {
          params.user = user.getName();
        }
        if (user.emails) {
          params.userEmails = user.emails;
        }
        if (activity.userId) {
          params.userId = activity.userId;
        }
      }
    }
    if (activity.boardId) {
      if (board.title.length > 0) {
        params.board = board.title;
      } else {
        params.board = '';
      }
      title = 'act-withBoardTitle';
      params.url = board.absoluteUrl();
      params.boardId = activity.boardId;
    }
    if (activity.oldBoardId) {
      const oldBoard = activity.oldBoard();
      if (oldBoard) {
        watchers = _.union(watchers, oldBoard.watchers || []);
        params.oldBoard = oldBoard.title;
        params.oldBoardId = activity.oldBoardId;
      }
    }
    if (activity.memberId) {
      participants = _.union(participants, [activity.memberId]);
      params.member = activity.member().getName();
    }
    if (activity.listId) {
      const list = activity.list();
      if (list.watchers !== undefined) {
        watchers = _.union(watchers, list.watchers || []);
      }
      params.list = list.title;
      params.listId = activity.listId;
    }
    if (activity.oldListId) {
      const oldList = activity.oldList();
      if (oldList) {
        watchers = _.union(watchers, oldList.watchers || []);
        params.oldList = oldList.title;
        params.oldListId = activity.oldListId;
      }
    }
    if (activity.oldSwimlaneId) {
      const oldSwimlane = activity.oldSwimlane();
      if (oldSwimlane) {
        watchers = _.union(watchers, oldSwimlane.watchers || []);
        params.oldSwimlane = oldSwimlane.title;
        params.oldSwimlaneId = activity.oldSwimlaneId;
      }
    }
    if (activity.cardId) {
      const card = activity.card();
      participants = _.union(participants, [card.userId], card.members || []);
      watchers = _.union(watchers, card.watchers || []);
      params.card = card.title;
      title = 'act-withCardTitle';
      params.url = card.absoluteUrl();
      params.cardId = activity.cardId;
    }
    if (activity.swimlaneId) {
      const swimlane = activity.swimlane();
      params.swimlane = swimlane.title;
      params.swimlaneId = activity.swimlaneId;
    }
    if (activity.commentId) {
      const comment = activity.comment();
      params.comment = comment.text;
      if (board) {
        const comment = params.comment;
        const knownUsers = board.members.map(member => {
          const u = Users.findOne(member.userId);
          if (u) {
            member.username = u.username;
            member.emails = u.emails;
          }
          return member;
        });
        const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username
        let currentMention;
        while ((currentMention = mentionRegex.exec(comment)) !== null) {
          /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[iI]gnored" }]*/
          const [ignored, quoteduser, simple] = currentMention;
          const username = quoteduser || simple;
          if (username === params.user) {
            // ignore commenter mention himself?
            continue;
          }

          if (activity.boardId && username === 'board_members') {
            // mentions all board members
            const knownUids = knownUsers.map(u => u.userId);
            watchers = _.union(watchers, [...knownUids]);
            title = 'act-atUserComment';
          } else if (activity.cardId && username === 'card_members') {
            // mentions all card members if assigned
            const card = activity.card();
            watchers = _.union(watchers, [...card.members]);
            title = 'act-atUserComment';
          } else {
            const atUser = _.findWhere(knownUsers, { username });
            if (!atUser) {
              continue;
            }

            const uid = atUser.userId;
            params.atUsername = username;
            params.atEmails = atUser.emails;
            title = 'act-atUserComment';
            watchers = _.union(watchers, [uid]);
          }

        }
      }
      params.commentId = comment._id;
    }
    if (activity.attachmentId) {
      const attachment = activity.attachment();
      params.attachment = attachment.original.name;
      params.attachmentId = attachment._id;
    }
    if (activity.checklistId) {
      const checklist = activity.checklist();
      if (checklist) {
        if (checklist.title) {
          params.checklist = checklist.title;
        }
      }
    }
    if (activity.checklistItemId) {
      const checklistItem = activity.checklistItem();
      if (checklistItem) {
        if (checklistItem.title) {
          params.checklistItem = checklistItem.title;
        }
      }
    }
    if (activity.customFieldId) {
      const customField = activity.customField();
      if (customField) {
        if (customField.name) {
          params.customField = customField.name;
        }
        if (activity.value) {
          params.customFieldValue = activity.value;
        }
      }
    }
    // Label activity did not work yet, unable to edit labels when tried this.
    //if (activity.labelId) {
    //  const label = activity.label();
    //  params.label = label.name;
    //  params.labelId = activity.labelId;
    //}
    if (
      (!activity.timeKey || activity.timeKey === 'dueAt') &&
      activity.timeValue
    ) {
      // due time reminder, if it doesn't have old value, it's a brand new set, need some differentiation
      title = activity.timeOldValue ? 'act-withDue' : 'act-newDue';
    }
    ['timeValue', 'timeOldValue'].forEach(key => {
      // copy time related keys & values to params
      const value = activity[key];
      if (value) params[key] = value;
    });
    if (board) {
      const BIGEVENTS = process.env.BIGEVENTS_PATTERN; // if environment BIGEVENTS_PATTERN is set, any activityType matching it is important
      if (BIGEVENTS) {
        try {
          const atype = activity.activityType;
          if (new RegExp(BIGEVENTS).exec(atype)) {
            watchers = _.union(
              watchers,
              board.activeMembers().map(member => member.userId),
            ); // notify all active members for important events
          }
        } catch (e) {
          // passed env var BIGEVENTS_PATTERN is not a valid regex
        }
      }

      const watchingUsers = _.pluck(
        _.where(board.watchers, { level: 'watching' }),
        'userId',
      );
      const trackingUsers = _.pluck(
        _.where(board.watchers, { level: 'tracking' }),
        'userId',
      );
      watchers = _.union(
        watchers,
        watchingUsers,
        _.intersection(participants, trackingUsers),
      );
    }
    Notifications.getUsers(watchers).forEach(user => {
      // don't notify a user of their own behavior
      if (user._id !== userId) {
        Notifications.notify(user, title, description, params);
      }
    });

    const integrations = Integrations.find({
      boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] },
      // type: 'outgoing-webhooks', // all types
      enabled: true,
      activities: { $in: [description, 'all'] },
    }).fetch();
    if (integrations.length > 0) {
      params.watchers = watchers;
      integrations.forEach(integration => {
        Meteor.call(
          'outgoingWebhooks',
          integration,
          description,
          params,
          () => {
            return;
          },
        );
      });
    }
  });
}

export default Activities;
<|endoftext|>


<commit_before>var    _ = require('underscore'),
     mbc = require('mbc-common'),
    conf = mbc.config.Caspa,
    search_options = mbc.config.Search,
    collections = mbc.config.Common.Collections,
    uuid = require('node-uuid'),
    logger = require("../logger")().addLogger('iobackends'),
    backboneio = require('backbone.io');

// Override mongoStore read method with custom
var searchWrapper = require('./searchWrapper.js');

var iobackends = module.exports = exports = function (db, publisher) {
    var self = this;
    this.middleware = {
        debug: function (req, res, next) {
            logger.debug('Backend:', req.backend);
            logger.debug('Method: ', req.method);
            logger.debug('Channel:', req.channel);
            logger.debug('Options:', JSON.stringify(req.options));
            logger.debug('Model:  ', JSON.stringify(req.model));
            next();
        },

        uuid: function (req, res, next) {
            if( req.method == 'create' && req.model._id === undefined) {
                req.model._id = uuid.v1();
            }
            next();
        },

        publishJSON: function (req, res, next) {
            publisher.publishJSON([req.backend, req.method].join('.'), { model: req.model });
            next();
        }
    };

    this.backends = {
        app: {
            use: [backboneio.middleware.configStore()]
        },
        transform: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Transforms,
                opts: { search: search_options.Transforms },
            }},
        media: {
            mongo: {
                db: db,
                collection: collections.Medias,
                opts: { search: search_options.Medias },
            }},
        piece: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Pieces,
                opts: { search: search_options.Pieces },
            }},
        list: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Lists,
                opts: { search: search_options.Lists },
            }},
        sched: {
            use: [this.middleware.uuid, this.middleware.publishJSON],
            mongo: {
                db: db,
                collection: collections.Scheds,
                opts: { search: search_options.Scheds },
            }},
        status: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Status,
                opts: { search: search_options.Status },
            }},
        frame: {
            use: [backboneio.middleware.memoryStore(db, 'progress', {})],
        },
        mostomessages: {
            mongo: {
                db: db,
                collection: collections.Mostomessages,
                opts: { search: search_options.Mostomessages },
            }},
        sketch: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Sketchs,
                opts: { search: search_options.Sketchs },
            }},
        user: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Auth,
            }},
    };

    /* process the backends object to streamline code */
    var binded = [];
    _(this.backends).each (function (backend) {
        backend.io = backboneio.createBackend();
        if (backend.use) {
            _(backend.use).each (function (usefn) {
                backend.io.use(usefn);
            });
        }
        if (backend.mongo) {
            var mongo = _.extend ({db: db, opts: {}}, backend.mongo);
            var fn = _.identity;
            binded.push (backend.mongo.collection);

            if (_.has(mongo.opts, 'search'))
                fn = searchWrapper;
            backend.io.use(
                fn(backboneio.middleware.mongoStore(mongo.db,
                                                    mongo.collection,
                                                    mongo.opts)));
        }
        backend.io.use (self.middleware.debug);
    });

    logger.info ('binding to mongo collections:', binded.join(', ') + '.');
};

iobackends.prototype.register_sync = function (collection, name) {
    logger.info ('binding sync on', name);
    var backend = this.get(name).io;

    function backend_emit (method, model) {
        var event = {create: 'created', read: 'updated', update: 'updated', delete: 'removed'};

        logger.warn ('emmiting', method);
        backend.emit (event[method], model);
    };

    function backend_sync (method, model, options) {
        var success = options.success || function (item) {logger.info  ('info:'  + item)};
        var error   = options.error   || function (err)  {logger.error ('error:' + err )};

        var res = {end: success, error: error};
        var req = {method: method, model: model.toJSON(), options: options};

        backend.handle (req, res, function(err, result) {
            if (err)
                logger.error ('while sync: ' + err);
            else
                backend_emit (method, result || model.toJSON());
        });
    }

    collection.osync = backend_sync;

    if (collection.delayed)
        while (p = collection.delayed.pop()) {
            backend_sync.apply(p);
        }
};

iobackends.prototype.emit = function (name, args) {
    _.apply(this.backends[name].emit, args);
};

iobackends.prototype.get_ios = function () {
    var ret = {};
    var self = this;
    _(_.keys(this.backends)).each (function (backend) {
        ret[backend + 'backend'] = self.backends[backend].io;
    });
    return ret;
};

iobackends.prototype.get = function (name) {
    return this.backends[name];
};

<commit_msg>Changing where is config located, we already in common<commit_after>var    _ = require('underscore'),
    config = require('config'),
    search_options = config.Search,
    collections = config.Common.Collections,
    uuid = require('node-uuid'),
    logger = require("../logger")().addLogger('iobackends'),
    backboneio = require('backbone.io');

// Override mongoStore read method with custom
var searchWrapper = require('./searchWrapper.js');

var iobackends = module.exports = exports = function (db, publisher) {
    var self = this;
    this.middleware = {
        debug: function (req, res, next) {
            logger.debug('Backend:', req.backend);
            logger.debug('Method: ', req.method);
            logger.debug('Channel:', req.channel);
            logger.debug('Options:', JSON.stringify(req.options));
            logger.debug('Model:  ', JSON.stringify(req.model));
            next();
        },

        uuid: function (req, res, next) {
            if( req.method == 'create' && req.model._id === undefined) {
                req.model._id = uuid.v1();
            }
            next();
        },

        publishJSON: function (req, res, next) {
            publisher.publishJSON([req.backend, req.method].join('.'), { model: req.model });
            next();
        }
    };

    this.backends = {
        app: {
            use: [backboneio.middleware.configStore()]
        },
        transform: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Transforms,
                opts: { search: search_options.Transforms },
            }},
        media: {
            mongo: {
                db: db,
                collection: collections.Medias,
                opts: { search: search_options.Medias },
            }},
        piece: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Pieces,
                opts: { search: search_options.Pieces },
            }},
        list: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Lists,
                opts: { search: search_options.Lists },
            }},
        sched: {
            use: [this.middleware.uuid, this.middleware.publishJSON],
            mongo: {
                db: db,
                collection: collections.Scheds,
                opts: { search: search_options.Scheds },
            }},
        status: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Status,
                opts: { search: search_options.Status },
            }},
        frame: {
            use: [backboneio.middleware.memoryStore(db, 'progress', {})],
        },
        mostomessages: {
            mongo: {
                db: db,
                collection: collections.Mostomessages,
                opts: { search: search_options.Mostomessages },
            }},
        sketch: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Sketchs,
                opts: { search: search_options.Sketchs },
            }},
        user: {
            use: [this.middleware.uuid],
            mongo: {
                db: db,
                collection: collections.Auth,
            }},
    };

    /* process the backends object to streamline code */
    var binded = [];
    _(this.backends).each (function (backend) {
        backend.io = backboneio.createBackend();
        if (backend.use) {
            _(backend.use).each (function (usefn) {
                backend.io.use(usefn);
            });
        }
        if (backend.mongo) {
            var mongo = _.extend ({db: db, opts: {}}, backend.mongo);
            var fn = _.identity;
            binded.push (backend.mongo.collection);

            if (_.has(mongo.opts, 'search'))
                fn = searchWrapper;
            backend.io.use(
                fn(backboneio.middleware.mongoStore(mongo.db,
                                                    mongo.collection,
                                                    mongo.opts)));
        }
        backend.io.use (self.middleware.debug);
    });

    logger.info ('binding to mongo collections:', binded.join(', ') + '.');
};

iobackends.prototype.register_sync = function (collection, name) {
    logger.info ('binding sync on', name);
    var backend = this.get(name).io;

    function backend_emit (method, model) {
        var event = {create: 'created', read: 'updated', update: 'updated', delete: 'removed'};

        logger.warn ('emmiting', method);
        backend.emit (event[method], model);
    };

    function backend_sync (method, model, options) {
        var success = options.success || function (item) {logger.info  ('info:'  + item)};
        var error   = options.error   || function (err)  {logger.error ('error:' + err )};

        var res = {end: success, error: error};
        var req = {method: method, model: model.toJSON(), options: options};

        backend.handle (req, res, function(err, result) {
            if (err)
                logger.error ('while sync: ' + err);
            else
                backend_emit (method, result || model.toJSON());
        });
    }

    collection.osync = backend_sync;

    if (collection.delayed)
        while (p = collection.delayed.pop()) {
            backend_sync.apply(p);
        }
};

iobackends.prototype.emit = function (name, args) {
    _.apply(this.backends[name].emit, args);
};

iobackends.prototype.get_ios = function () {
    var ret = {};
    var self = this;
    _(_.keys(this.backends)).each (function (backend) {
        ret[backend + 'backend'] = self.backends[backend].io;
    });
    return ret;
};

iobackends.prototype.get = function (name) {
    return this.backends[name];
};

<|endoftext|>


<commit_before>import config from 'config';
import _ from 'lodash';
import Promise from 'bluebird';
import juice from 'juice';
import nodemailer from 'nodemailer';
import debugLib from 'debug';
import templates from './emailTemplates';
import activities from '../constants/activities';
import {isEmailInternal} from './utils';
import crypto from 'crypto';

const debug = debugLib('email');

const render = (template, data) => {
    data.logoNotSvg = data.group && data.group.logo && !data.group.logo.endsWith('.svg');
    data = _.merge({}, data);
    delete data.config;
    data.config = { host: config.host };
    debug(`Preview email template: http://localhost:3060/templates/email/${template}?data=${encodeURIComponent(JSON.stringify(data))}`);
    return juice(templates[template](data));
};

const generateUnsubscribeToken = (email, groupSlug, type) => {
  const uid = `${email}.${groupSlug}.${type}.${config.keys.opencollective.secret}`;
  const token = crypto.createHash('md5').update(uid).digest("hex");
  return token;
}

/*
 * Gets the body from a string (usually a template)
 */
const getBody = str => str.split('\n').slice(2).join('\n');

/*
 * Appends appropriate prefix and cleans up subject
 */
const getSubject = str => {
    let subj = '';
    if (process.env.NODE_ENV === 'staging') {
      subj += '[STAGING] ';
    } else if (process.env.NODE_ENV !== 'production'){
      subj += '[TESTING] ';
    }
    subj += str.split('\n')[0].replace(/^Subject: ?/i, '');
    return subj;
};

/*
 * sends an email message to a recipient with given subject and body
 */
const sendMessage = (recipients, subject, html, options) => {
  options = options || {};
  debug("email: ", recipients, subject);

  if (!_.isArray(recipients)) recipients = [ recipients ];

  recipients = recipients.filter(recipient => {
    if (!recipient.match(/.+@.+\..+/)) {
      debug(`${recipient} is an invalid email address, skipping`);
      return false;
    }
    // if not in production, only send out emails to bcc'd opencollective address
    if (process.env.NODE_ENV !== 'production' && !isEmailInternal(recipient)) {
      debug(`${recipient} is an external email address, skipping in development environment`);
      return false;
    } else {
      return true;
    }
  });

  debug(`sending email to ${recipients.join(', ')}`);
  if (recipients.length === 0) {
    debug("No recipient to send to, only sending to ops");
  }

  if (config.mailgun.user) {
    const mailgun = nodemailer.createTransport({
      service: 'Mailgun',
      auth: {
        user: config.mailgun.user,
        pass: config.mailgun.password
      }
    });

    return new Promise((resolve, reject) => {
      mailgun.sendMail({
        from: options.from || config.email.from,
        to: recipients.join(', '),
        bcc: `ops@opencollective.com,${options.bcc}`,
        subject,
        html
      }, (err, info) => {
        if (err) {
          return reject(err);
        } else {
          return resolve(info);
        }
      })
    });
  } else {
    console.warn("Warning: No mail sent - Mailgun is not configured");
    return Promise.resolve();
  }
};

const getNotificationLabel = (template, recipient) => {

  const notificationTypeLabels = {
    'group.monthlyreport': 'monthly reports',
    'group.transaction.created': 'notifications of new transactions for this collective',
    'email.approve': 'notifications of new emails pending approval',
    'email.message': recipient
  }

  return notificationTypeLabels[template];

};

/*
 * Given a template, recipient and data, generates email.
 */

const generateEmailFromTemplate = (template, recipient, data, options) => {

  options = options || {};

  if (template === 'thankyou') {
    if (data.group.name.match(/WWCode/i))
      template += '.wwcode';
    if (data.group.name.match(/ispcwa/i))
      template += '.ispcwa';
    if (data.group.slug === 'brusselstogether')
      template = 'thankyou.brusselstogether';
    if (_.contains(['lesbarbares', 'nuitdebout', 'laprimaire'], data.group.slug)) {
      template += '.fr';

      if (data.group.slug === 'laprimaire')
        template = 'thankyou.laprimaire';

      // xdamman: hack
      switch (data.interval) {
        case 'month':
          data.interval = 'mois';
          break;
        case 'year':
          data.interval = 'an';
          break;
      }
    }
  }

  if (template === 'group.transaction.created') {
    template = (data.transaction.amount > 0) ? 'group.donation.created' : 'group.expense.created';
    if (data.user && data.user.twitterHandle) {
      const groupMention = (data.group.twitterHandle) ? `@${data.group.twitterHandle}` : data.group.name;
      const text = `Hi @${data.user.twitterHandle} thanks for your donation to ${groupMention} https://opencollective.com/${data.group.slug} 🎉😊`;
      data.tweet = {
        text,
        encoded: encodeURIComponent(text)
      };
    }
  }

  const slug = (data.group && data.group.slug) ? data.group.slug : 'undefined';

  data.unsubscribeUrl = `${config.host.website}/api/services/email/unsubscribe/${encodeURIComponent(options.bcc || recipient)}/${slug}/${options.type || template}/${generateUnsubscribeToken(options.bcc || recipient, slug, options.type || template)}`;
  data.notificationTypeLabel = getNotificationLabel(template, recipient);
  data.config = config;

  if (!templates[template]) {
    return Promise.reject(new Error("Invalid email template"));
  }
  return Promise.resolve(render(template, data));
};

/*
 * Given a template, recipient and data, generates email and sends it.
 * Deprecated. Should use sendMessageFromActivity() for sending new emails.
 */
const generateEmailFromTemplateAndSend = (template, recipient, data, options) => {
  return generateEmailFromTemplate(template, recipient, data, options)
    .then(templateString => emailLib.sendMessage(recipient, getSubject(templateString), getBody(templateString), options));
};

/*
 * Given an activity, it sends out an email to the right people and right template
 */
const sendMessageFromActivity = (activity, notification) => {
  switch (activity.type) {
    case activities.GROUP_TRANSACTION_CREATED:
      return generateEmailFromTemplateAndSend('group.transaction.created', notification.User.email, activity.data);
    case activities.GROUP_EXPENSE_CREATED:
      return generateEmailFromTemplateAndSend('group.expense.created', notification.User.email, activity.data);
    default:
      return Promise.resolve();
  }
}

const emailLib = {
  render,
  getBody,
  getSubject,
  sendMessage,
  generateEmailFromTemplate,
  send: generateEmailFromTemplateAndSend,
  sendMessageFromActivity
};

export default emailLib;
<commit_msg>Export email preview in `/tmp/:template.:slug.html`<commit_after>import config from 'config';
import _ from 'lodash';
import Promise from 'bluebird';
import juice from 'juice';
import nodemailer from 'nodemailer';
import debugLib from 'debug';
import templates from './emailTemplates';
import activities from '../constants/activities';
import {isEmailInternal} from './utils';
import crypto from 'crypto';
import fs from 'fs';

const debug = debugLib('email');

const render = (template, data) => {
    data.logoNotSvg = data.group && data.group.logo && !data.group.logo.endsWith('.svg');
    data = _.merge({}, data);
    delete data.config;
    data.config = { host: config.host };
    const html = juice(templates[template](data));

    // When in preview mode, we export an HTML version of the email in `/tmp/:template.:slug.html`
    if (process.env.DEBUG && process.env.DEBUG.match(/preview/)) {
      const filepath = `/tmp/${template}.${data.group && data.group.slug}.html`;
      const script = `<script>data=${JSON.stringify(data)};</script>`;
      fs.writeFileSync(filepath, `${script}\n\n${html}`);
      console.log(`Preview email template: file://${filepath}`);
    }
    return html;
};

const generateUnsubscribeToken = (email, groupSlug, type) => {
  const uid = `${email}.${groupSlug}.${type}.${config.keys.opencollective.secret}`;
  const token = crypto.createHash('md5').update(uid).digest("hex");
  return token;
}

/*
 * Gets the body from a string (usually a template)
 */
const getBody = str => str.split('\n').slice(2).join('\n');

/*
 * Appends appropriate prefix and cleans up subject
 */
const getSubject = str => {
    let subj = '';
    if (process.env.NODE_ENV === 'staging') {
      subj += '[STAGING] ';
    } else if (process.env.NODE_ENV !== 'production'){
      subj += '[TESTING] ';
    }
    subj += str.split('\n')[0].replace(/^Subject: ?/i, '');
    return subj;
};

/*
 * sends an email message to a recipient with given subject and body
 */
const sendMessage = (recipients, subject, html, options) => {
  options = options || {};
  debug("email: ", recipients, subject);

  if (!_.isArray(recipients)) recipients = [ recipients ];

  recipients = recipients.filter(recipient => {
    if (!recipient || !recipient.match(/.+@.+\..+/)) {
      debug(`${recipient} is an invalid email address, skipping`);
      return false;
    }
    // if not in production, only send out emails to bcc'd opencollective address
    if (process.env.NODE_ENV !== 'production' && !isEmailInternal(recipient)) {
      debug(`${recipient} is an external email address, skipping in development environment`);
      return false;
    } else {
      return true;
    }
  });

  debug(`sending email to ${recipients.join(', ')}`);
  if (recipients.length === 0) {
    debug("No recipient to send to, only sending to ops");
  }

  if (config.mailgun.user) {
    const mailgun = nodemailer.createTransport({
      service: 'Mailgun',
      auth: {
        user: config.mailgun.user,
        pass: config.mailgun.password
      }
    });

    return new Promise((resolve, reject) => {
      mailgun.sendMail({
        from: options.from || config.email.from,
        to: recipients.join(', '),
        bcc: `ops@opencollective.com,${options.bcc}`,
        subject,
        html
      }, (err, info) => {
        if (err) {
          return reject(err);
        } else {
          return resolve(info);
        }
      })
    });
  } else {
    console.warn("Warning: No mail sent - Mailgun is not configured");
    return Promise.resolve();
  }
};

const getNotificationLabel = (template, recipient) => {

  const notificationTypeLabels = {
    'group.monthlyreport': 'monthly reports',
    'group.transaction.created': 'notifications of new transactions for this collective',
    'email.approve': 'notifications of new emails pending approval',
    'email.message': recipient
  }

  return notificationTypeLabels[template];

};

/*
 * Given a template, recipient and data, generates email.
 */

const generateEmailFromTemplate = (template, recipient, data, options) => {

  options = options || {};

  if (template === 'thankyou') {
    if (data.group.name.match(/WWCode/i))
      template += '.wwcode';
    if (data.group.name.match(/ispcwa/i))
      template += '.ispcwa';
    if (data.group.slug === 'brusselstogether')
      template = 'thankyou.brusselstogether';
    if (_.contains(['lesbarbares', 'nuitdebout', 'laprimaire'], data.group.slug)) {
      template += '.fr';

      if (data.group.slug === 'laprimaire')
        template = 'thankyou.laprimaire';

      // xdamman: hack
      switch (data.interval) {
        case 'month':
          data.interval = 'mois';
          break;
        case 'year':
          data.interval = 'an';
          break;
      }
    }
  }

  if (template === 'group.transaction.created') {
    template = (data.transaction.amount > 0) ? 'group.donation.created' : 'group.expense.created';
    if (data.user && data.user.twitterHandle) {
      const groupMention = (data.group.twitterHandle) ? `@${data.group.twitterHandle}` : data.group.name;
      const text = `Hi @${data.user.twitterHandle} thanks for your donation to ${groupMention} https://opencollective.com/${data.group.slug} 🎉😊`;
      data.tweet = {
        text,
        encoded: encodeURIComponent(text)
      };
    }
  }

  const slug = (data.group && data.group.slug) ? data.group.slug : 'undefined';

  data.unsubscribeUrl = `${config.host.website}/api/services/email/unsubscribe/${encodeURIComponent(options.bcc || recipient)}/${slug}/${options.type || template}/${generateUnsubscribeToken(options.bcc || recipient, slug, options.type || template)}`;
  data.notificationTypeLabel = getNotificationLabel(template, recipient);
  data.config = config;

  if (!templates[template]) {
    return Promise.reject(new Error("Invalid email template"));
  }
  return Promise.resolve(render(template, data));
};

/*
 * Given a template, recipient and data, generates email and sends it.
 * Deprecated. Should use sendMessageFromActivity() for sending new emails.
 */
const generateEmailFromTemplateAndSend = (template, recipient, data, options) => {
  return generateEmailFromTemplate(template, recipient, data, options)
    .then(templateString => emailLib.sendMessage(recipient, getSubject(templateString), getBody(templateString), options));
};

/*
 * Given an activity, it sends out an email to the right people and right template
 */
const sendMessageFromActivity = (activity, notification) => {
  switch (activity.type) {
    case activities.GROUP_TRANSACTION_CREATED:
      return generateEmailFromTemplateAndSend('group.transaction.created', notification.User.email, activity.data);
    case activities.GROUP_EXPENSE_CREATED:
      return generateEmailFromTemplateAndSend('group.expense.created', notification.User.email, activity.data);
    default:
      return Promise.resolve();
  }
}

const emailLib = {
  render,
  getBody,
  getSubject,
  sendMessage,
  generateEmailFromTemplate,
  send: generateEmailFromTemplateAndSend,
  sendMessageFromActivity
};

export default emailLib;
<|endoftext|>


<commit_before>var ais = require("ais");
var moment = require("moment");
var async = require("async");
var validator = require("validator");
var inreach = require("inreach");

module.exports = {

	// READ AIS POSITION
	readAISPosition: function(userId, mmsi, callback) {

		if(!mmsi) return callback(null);

		if(!validator.isNumeric(mmsi) || mmsi.trim().length !== 9) {
			return callback(null);
		}

		mmsi = mmsi.trim();

		// try cache
		global.CACHE_ais_positions.find({"_id": mmsi}).next(function(err, data) {

			// not a cache hit
			if(err || !data) {

				try {
					ais.get(mmsi, function(pos, more) {

						// store value in cache
						global.CACHE_ais_positions.insert({
							"_id": mmsi,
							"pos": pos,
							"more": more,
							"stored": moment.utc().toDate()
						});

						// store value in positions history
						try {
							global.positions.insert({
								"_id": {
									"user": userId,
									"time": more.time
								},
								"pos": pos,
								"spd": more.speed,
								"crs": more.course,
								"src": {
									"type": "AIS",
									"mmsi": mmsi,
									"name": more.name
								}
							});
						} catch (e) {

						}

						return callback(pos, more);
					});
				}
				catch(e) {
					return callback(null, null);
				}
			}
			else {
				return callback(data.pos, data.more);
			}
		});
	},

	// READ INREACH POSITION
	readInReachPosition: function(userId, username, callback) {

		if(!username || username.trim().length < 2) return callback(null);

		username = username.trim();

		// try cache
		global.CACHE_inreach_positions.find({"_id": username}).next(function(err, data) {

			// not a cache hit
			if(err || !data) {

				try {
					inreach.get(username, function(pos, more) {

						// store value in cache
						global.CACHE_inreach_positions.insert({
							"_id": username,
							"pos": pos,
							"more": more,
							"stored": moment.utc().toDate()
						});

						// store position in history
						try {
							global.positions.insert({
								"_id": {
									"user": userId,
									"time": more.time
								},
								"pos": pos,
								"spd": more.speed,
								"crs": more.course,
								"src": {
									"type": "InReach",
									"username": username,
									"name": more.name
								}
							});
						}
						catch(e) {

						}

						return callback(pos, more);
					});
				}
				catch(e) {
					return callback(null, null);
				}
			}
			else {
				return callback(data.pos, data.more);
			}
		});
	},

	// READ POSITION
	readPosition: function(user, callback) {

		var profile = user.profile;

		// check for inreach
		if(profile && "inreach" in profile && profile.inreach.length > 0) {
			module.exports.readInReachPosition(user._id, profile.inreach, function(pos, more) {

				// no position? fallback to mmsi / ais positioning
				if(pos === null) {

					if(profile && "mmsi" in profile && profile.mmsi.length > 0) {
						module.exports.readAISPosition(user._id, profile.mmsi, function(pos, more) {
							return callback(pos, more, "mmsi");
						});
					}
					else {
						return callback(null, null, null);
					}
				}
				else {
					return callback(pos, more, "inreach");
				}
			});
		}

		// check for mmsi
		else if(profile && "mmsi" in profile && profile.mmsi.length > 0) {
			module.exports.readAISPosition(user._id, profile.mmsi, function(pos, more) {
				return callback(pos, more, "mmsi");
			});
		}

		// nothing here to see
		else {
			return callback(null, null, null);
		}
	},

	// LAST
	// retriece the last n positions per channel
	last: function(req, res) {

		var n = parseInt(req.params.n);

		if(n > 0) {

			// fetch positions
			global.positions.find({
				"_id.user": req.params.id
			}).sort({
				"_id.time": -1
			}).project({
				"time": false
			}).limit(n).toArray(function(err, positions) {

				// handle the error
				if(err || !positions) {
					return res.status(500).send(err || "unable to retrieve positions");
				}

				// remap properties of array
				positions = positions.map((p) => {

					// remap time
					p["time"] = p["_id"]["time"];
					delete p["_id"];

					return p;
				});

				return res.send(positions);
			});
		}
		else {
			return res.send([]);
		}
	},

	// GEOLOCATIONS
	geolocations: function(req, res) {

		// check if there is a cached version available
		global.CACHE_geolocations.findOne({"_id": "geolocations"}, function(err, data) {

			if(err || !data) {

				global.videos.find({
					"geo": {
						"$exists": true
					}
				}).project({
					"_id": true,
					"geo": true
				}).toArray(function(err, locations) {

					// handle the error
					if(err || !locations) {
						return res.status(500).send(err || "unable to retrieve geolocations");
					}

					var geojson = {
						"type": "FeatureCollection",
						"features": []
					};

					// iterate over locations and build geojson
					for(var l in locations) {

						// iterate over the geometries of the locations
						for(var g in locations[l].geo.geometries)
						var feature = {
							"type": "Feature",
							"geometry": locations[l].geo.geometries[g],
							"properties": {
								"v": locations[l]["_id"]
							}
						}

						geojson.features.push(feature);
					}

					// store geolocations cache
					global.CACHE_geolocations.insert({
						"_id": "geolocations",
						"time": moment().toDate()
						"data": geojson
					});

					return res.send(geojson);
				});
			}
			else {
				return res.send(data);
			}

		});

	}
};
<commit_msg>no message<commit_after>var ais = require("ais");
var moment = require("moment");
var async = require("async");
var validator = require("validator");
var inreach = require("inreach");

module.exports = {

	// READ AIS POSITION
	readAISPosition: function(userId, mmsi, callback) {

		if(!mmsi) return callback(null);

		if(!validator.isNumeric(mmsi) || mmsi.trim().length !== 9) {
			return callback(null);
		}

		mmsi = mmsi.trim();

		// try cache
		global.CACHE_ais_positions.find({"_id": mmsi}).next(function(err, data) {

			// not a cache hit
			if(err || !data) {

				try {
					ais.get(mmsi, function(pos, more) {

						// store value in cache
						global.CACHE_ais_positions.insert({
							"_id": mmsi,
							"pos": pos,
							"more": more,
							"stored": moment.utc().toDate()
						});

						// store value in positions history
						try {
							global.positions.insert({
								"_id": {
									"user": userId,
									"time": more.time
								},
								"pos": pos,
								"spd": more.speed,
								"crs": more.course,
								"src": {
									"type": "AIS",
									"mmsi": mmsi,
									"name": more.name
								}
							});
						} catch (e) {

						}

						return callback(pos, more);
					});
				}
				catch(e) {
					return callback(null, null);
				}
			}
			else {
				return callback(data.pos, data.more);
			}
		});
	},

	// READ INREACH POSITION
	readInReachPosition: function(userId, username, callback) {

		if(!username || username.trim().length < 2) return callback(null);

		username = username.trim();

		// try cache
		global.CACHE_inreach_positions.find({"_id": username}).next(function(err, data) {

			// not a cache hit
			if(err || !data) {

				try {
					inreach.get(username, function(pos, more) {

						// store value in cache
						global.CACHE_inreach_positions.insert({
							"_id": username,
							"pos": pos,
							"more": more,
							"stored": moment.utc().toDate()
						});

						// store position in history
						try {
							global.positions.insert({
								"_id": {
									"user": userId,
									"time": more.time
								},
								"pos": pos,
								"spd": more.speed,
								"crs": more.course,
								"src": {
									"type": "InReach",
									"username": username,
									"name": more.name
								}
							});
						}
						catch(e) {

						}

						return callback(pos, more);
					});
				}
				catch(e) {
					return callback(null, null);
				}
			}
			else {
				return callback(data.pos, data.more);
			}
		});
	},

	// READ POSITION
	readPosition: function(user, callback) {

		var profile = user.profile;

		// check for inreach
		if(profile && "inreach" in profile && profile.inreach.length > 0) {
			module.exports.readInReachPosition(user._id, profile.inreach, function(pos, more) {

				// no position? fallback to mmsi / ais positioning
				if(pos === null) {

					if(profile && "mmsi" in profile && profile.mmsi.length > 0) {
						module.exports.readAISPosition(user._id, profile.mmsi, function(pos, more) {
							return callback(pos, more, "mmsi");
						});
					}
					else {
						return callback(null, null, null);
					}
				}
				else {
					return callback(pos, more, "inreach");
				}
			});
		}

		// check for mmsi
		else if(profile && "mmsi" in profile && profile.mmsi.length > 0) {
			module.exports.readAISPosition(user._id, profile.mmsi, function(pos, more) {
				return callback(pos, more, "mmsi");
			});
		}

		// nothing here to see
		else {
			return callback(null, null, null);
		}
	},

	// LAST
	// retriece the last n positions per channel
	last: function(req, res) {

		var n = parseInt(req.params.n);

		if(n > 0) {

			// fetch positions
			global.positions.find({
				"_id.user": req.params.id
			}).sort({
				"_id.time": -1
			}).project({
				"time": false
			}).limit(n).toArray(function(err, positions) {

				// handle the error
				if(err || !positions) {
					return res.status(500).send(err || "unable to retrieve positions");
				}

				// remap properties of array
				positions = positions.map((p) => {

					// remap time
					p["time"] = p["_id"]["time"];
					delete p["_id"];

					return p;
				});

				return res.send(positions);
			});
		}
		else {
			return res.send([]);
		}
	},

	// GEOLOCATIONS
	geolocations: function(req, res) {

		// check if there is a cached version available
		global.CACHE_geolocations.findOne({"_id": "geolocations"}, function(err, data) {

			if(err || !data) {

				global.videos.find({
					"geo": {
						"$exists": true
					}
				}).project({
					"_id": true,
					"geo": true
				}).toArray(function(err, locations) {

					// handle the error
					if(err || !locations) {
						return res.status(500).send(err || "unable to retrieve geolocations");
					}

					var geojson = {
						"type": "FeatureCollection",
						"features": []
					};

					// iterate over locations and build geojson
					for(var l in locations) {

						// iterate over the geometries of the locations
						for(var g in locations[l].geo.geometries)
						var feature = {
							"type": "Feature",
							"geometry": locations[l].geo.geometries[g],
							"properties": {
								"v": locations[l]["_id"]
							}
						}

						geojson.features.push(feature);
					}

					// store geolocations cache
					global.CACHE_geolocations.insert({
						"_id": "geolocations",
						"time": moment().toDate(),
						"data": geojson
					});

					return res.send(geojson);
				});
			}
			else {
				return res.send(data);
			}

		});

	}
};
<|endoftext|>


<commit_before>import {
  action,
  computed,
  observable,
} from 'mobx';
import semver from 'semver';
import localStorage from 'mobx-localstorage';

import { FeatureStore } from '../utils/FeatureStore';
import { ANNOUNCEMENTS_ROUTES, GA_CATEGORY_ANNOUNCEMENTS } from '.';
import { getAnnouncementRequest, getChangelogRequest, getCurrentVersionRequest } from './api';
import { announcementActions } from './actions';
import { createActionBindings } from '../utils/ActionBinding';
import { createReactions } from '../../stores/lib/Reaction';
import { gaEvent } from '../../lib/analytics';
import { matchRoute } from '../../helpers/routing-helpers';

const LOCAL_STORAGE_KEY = 'announcements';

const debug = require('debug')('Franz:feature:announcements:store');

export class AnnouncementsStore extends FeatureStore {
  @observable targetVersion = null;

  @observable isFeatureActive = false;

  @computed get changelog() {
    return getChangelogRequest.result;
  }

  @computed get announcement() {
    if (!this.stores || !getAnnouncementRequest.result) return null;
    const { locale, defaultLocale } = this.stores.app;
    const announcement = getAnnouncementRequest.result;
    // User locale
    if (announcement[locale]) return announcement[locale];
    // Default locale
    if (announcement[defaultLocale]) return announcement[defaultLocale];
    // No locales specified
    return announcement;
  }

  @computed get areNewsAvailable() {
    const isChangelogAvailable = getChangelogRequest.wasExecuted && !!this.changelog;
    const isAnnouncementAvailable = getAnnouncementRequest.wasExecuted && !!this.announcement;
    return isChangelogAvailable || isAnnouncementAvailable;
  }

  @computed get settings() {
    return localStorage.getItem(LOCAL_STORAGE_KEY) || {};
  }

  @computed get lastSeenAnnouncementVersion() {
    return this.settings.lastSeenAnnouncementVersion || null;
  }

  @computed get currentVersion() {
    return getCurrentVersionRequest.result;
  }

  @computed get isNewUser() {
    return this.stores.settings.stats.appStarts <= 1;
  }

  async start(stores, actions) {
    debug('AnnouncementsStore::start');
    this.stores = stores;
    this.actions = actions;
    getCurrentVersionRequest.execute();

    this._registerActions(createActionBindings([
      [announcementActions.show, this._showAnnouncement],
    ]));

    this._reactions = createReactions([
      this._showAnnouncementOnRouteMatch,
      this._showAnnouncementToUsersWhoUpdatedApp,
      this._fetchAnnouncements,
    ]);
    this._registerReactions(this._reactions);
    this.isFeatureActive = true;
  }

  stop() {
    super.stop();
    debug('AnnouncementsStore::stop');
    this.isFeatureActive = false;
  }

  // ======= HELPERS ======= //

  _updateSettings = (changes) => {
    localStorage.setItem(LOCAL_STORAGE_KEY, {
      ...this.settings,
      ...changes,
    });
  };

  // ======= ACTIONS ======= //

  @action _showAnnouncement = ({ targetVersion } = {}) => {
    const { router } = this.stores;
    this.targetVersion = targetVersion || this.currentVersion;
    this._updateSettings({
      lastSeenAnnouncementVersion: this.currentVersion,
    });
    const targetRoute = `/announcements/${this.targetVersion}`;
    if (router.location.pathname !== targetRoute) {
      this.stores.router.push(targetRoute);
    }
    gaEvent(GA_CATEGORY_ANNOUNCEMENTS, 'show');
  };

  // ======= REACTIONS ========

  _showAnnouncementToUsersWhoUpdatedApp = () => {
    const { announcement, isNewUser } = this;
    // Check if there is an announcement and don't show announcements to new users
    if (!announcement || isNewUser) return;

    // Check if the user has already used current version (= has seen the announcement)
    const { currentVersion, lastSeenAnnouncementVersion } = this;
    if (semver.gt(currentVersion, lastSeenAnnouncementVersion || '0.0.0')) {
      debug(`${currentVersion} > ${lastSeenAnnouncementVersion}: announcement is shown`);
      this._showAnnouncement();
    }
  };

  _fetchAnnouncements = () => {
    const targetVersion = this.targetVersion || this.currentVersion;
    if (!targetVersion) return;
    getChangelogRequest.reset().execute(targetVersion);
    getAnnouncementRequest.reset().execute(targetVersion);
  };

  _showAnnouncementOnRouteMatch = () => {
    const { router } = this.stores;
    const match = matchRoute(ANNOUNCEMENTS_ROUTES.TARGET, router.location.pathname);
    if (match) {
      const targetVersion = match.id;
      this._showAnnouncement({ targetVersion });
    }
  }
}
<commit_msg>fix default locale language fallback<commit_after>import {
  action,
  computed,
  observable,
} from 'mobx';
import semver from 'semver';
import localStorage from 'mobx-localstorage';

import { FeatureStore } from '../utils/FeatureStore';
import { ANNOUNCEMENTS_ROUTES, GA_CATEGORY_ANNOUNCEMENTS } from '.';
import { getAnnouncementRequest, getChangelogRequest, getCurrentVersionRequest } from './api';
import { announcementActions } from './actions';
import { createActionBindings } from '../utils/ActionBinding';
import { createReactions } from '../../stores/lib/Reaction';
import { gaEvent } from '../../lib/analytics';
import { matchRoute } from '../../helpers/routing-helpers';
import { DEFAULT_APP_SETTINGS } from '../../config';

const LOCAL_STORAGE_KEY = 'announcements';

const debug = require('debug')('Franz:feature:announcements:store');

export class AnnouncementsStore extends FeatureStore {
  @observable targetVersion = null;

  @observable isFeatureActive = false;

  @computed get changelog() {
    return getChangelogRequest.result;
  }

  @computed get announcement() {
    if (!this.stores || !getAnnouncementRequest.result) return null;
    const { locale } = this.stores.app;
    const announcement = getAnnouncementRequest.result;
    // User locale
    if (announcement[locale]) return announcement[locale];
    // Default locale
    if (announcement[DEFAULT_APP_SETTINGS.fallbackLocale]) return announcement[DEFAULT_APP_SETTINGS.fallbackLocale];
    // No locales specified
    return announcement;
  }

  @computed get areNewsAvailable() {
    const isChangelogAvailable = getChangelogRequest.wasExecuted && !!this.changelog;
    const isAnnouncementAvailable = getAnnouncementRequest.wasExecuted && !!this.announcement;
    return isChangelogAvailable || isAnnouncementAvailable;
  }

  @computed get settings() {
    return localStorage.getItem(LOCAL_STORAGE_KEY) || {};
  }

  @computed get lastSeenAnnouncementVersion() {
    return this.settings.lastSeenAnnouncementVersion || null;
  }

  @computed get currentVersion() {
    return getCurrentVersionRequest.result;
  }

  @computed get isNewUser() {
    return this.stores.settings.stats.appStarts <= 1;
  }

  async start(stores, actions) {
    debug('AnnouncementsStore::start');
    this.stores = stores;
    this.actions = actions;
    getCurrentVersionRequest.execute();

    this._registerActions(createActionBindings([
      [announcementActions.show, this._showAnnouncement],
    ]));

    this._reactions = createReactions([
      this._showAnnouncementOnRouteMatch,
      this._showAnnouncementToUsersWhoUpdatedApp,
      this._fetchAnnouncements,
    ]);
    this._registerReactions(this._reactions);
    this.isFeatureActive = true;
  }

  stop() {
    super.stop();
    debug('AnnouncementsStore::stop');
    this.isFeatureActive = false;
  }

  // ======= HELPERS ======= //

  _updateSettings = (changes) => {
    localStorage.setItem(LOCAL_STORAGE_KEY, {
      ...this.settings,
      ...changes,
    });
  };

  // ======= ACTIONS ======= //

  @action _showAnnouncement = ({ targetVersion } = {}) => {
    const { router } = this.stores;
    this.targetVersion = targetVersion || this.currentVersion;
    this._updateSettings({
      lastSeenAnnouncementVersion: this.currentVersion,
    });
    const targetRoute = `/announcements/${this.targetVersion}`;
    if (router.location.pathname !== targetRoute) {
      this.stores.router.push(targetRoute);
    }
    gaEvent(GA_CATEGORY_ANNOUNCEMENTS, 'show');
  };

  // ======= REACTIONS ========

  _showAnnouncementToUsersWhoUpdatedApp = () => {
    const { announcement, isNewUser } = this;
    // Check if there is an announcement and don't show announcements to new users
    if (!announcement || isNewUser) return;

    // Check if the user has already used current version (= has seen the announcement)
    const { currentVersion, lastSeenAnnouncementVersion } = this;
    if (semver.gt(currentVersion, lastSeenAnnouncementVersion || '0.0.0')) {
      debug(`${currentVersion} > ${lastSeenAnnouncementVersion}: announcement is shown`);
      this._showAnnouncement();
    }
  };

  _fetchAnnouncements = () => {
    const targetVersion = this.targetVersion || this.currentVersion;
    if (!targetVersion) return;
    getChangelogRequest.reset().execute(targetVersion);
    getAnnouncementRequest.reset().execute(targetVersion);
  };

  _showAnnouncementOnRouteMatch = () => {
    const { router } = this.stores;
    const match = matchRoute(ANNOUNCEMENTS_ROUTES.TARGET, router.location.pathname);
    if (match) {
      const targetVersion = match.id;
      this._showAnnouncement({ targetVersion });
    }
  }
}
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.view',
  name: 'EditColumnsView',
  extends: 'foam.u2.Element',

  requires: [
    'foam.u2.md.CheckBox'
  ],

  properties: [
    {
      name: 'columns'
    },
    {
      name: 'table'
    },
    {
      name: 'columns_'
    }
  ],

  css: `
    ^ {
      text-align: left;
      padding: 10px;
      width: 125px;
    }
    ^ label {
      margin-top: 5px;
    }
  `,

  methods: [
    function initE() {
      var index = 0;
      this
        .addClass(this.myClass())
        .forEach(this.columns, function(columnName) {
          var axiom = this.table.getAxiomByName(columnName);
          var localStorageKey = this.table.id + '.' + columnName;
          var valueFromLocalStorage = localStorage.getItem(localStorageKey);

          // This will be true only when seeing a new model for the first time.
          if ( valueFromLocalStorage === null ) {
            localStorage.setItem(localStorageKey, 'Y');
            valueFromLocalStorage = true;
          }

          var checkBox = this.CheckBox.create({
            label: axiom.label,

            // Double negate to cast String to Boolean.
            data: !! valueFromLocalStorage
          });

          checkBox.data$.sub(this.updateCols(axiom, index++, localStorageKey));

          this.start().tag(checkBox).end();
        });
    }
  ],

  listeners: [
    function updateCols(axiom, index, localStorageKey) {
      return (_, __, ___, propSlot) => {
        var checked = propSlot.get();
        if ( checked ) {
          // Any truthy string will suffice.
          localStorage.setItem(localStorageKey, 'Y');

          // Put the column back in the correct position.
          this.columns_.splice(index, 0, axiom);

          // Force the view to update.
          this.columns_ = this.columns_.slice();
        } else {
          // Set to the only falsey string. We can't just set the items to true
          // and false because localStorage only stores strings as values.
          localStorage.setItem(localStorageKey, '');

          this.columns_ = this.columns_.filter((col) => col.name !== axiom.name);
        }
      };
    }
  ]
});
<commit_msg>Change the way we remember which columns to show/hide<commit_after>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.view',
  name: 'EditColumnsView',
  extends: 'foam.u2.Element',

  requires: [
    'foam.u2.md.CheckBox'
  ],

  properties: [
    {
      name: 'columns'
    },
    {
      name: 'table'
    },
    {
      name: 'columns_'
    }
  ],

  css: `
    ^ {
      text-align: left;
      padding: 10px;
      width: 125px;
    }
    ^ label {
      margin-top: 5px;
    }
  `,

  methods: [
    function initE() {
      var index = 0;
      this
        .addClass(this.myClass())
        .forEach(this.columns, function(columnName) {
          var axiom = this.table.getAxiomByName(columnName);
          var localStorageKey = this.table.id + '.' + columnName;

          var checkBox = this.CheckBox.create({
            label: axiom.label,
            data: localStorage.getItem(localStorageKey) === null
          });

          checkBox.data$.sub(this.updateCols(axiom, index++, localStorageKey));

          this.start().tag(checkBox).end();
        });
    }
  ],

  listeners: [
    function updateCols(axiom, index, localStorageKey) {
      return (_, __, ___, propSlot) => {
        var checked = propSlot.get();
        if ( checked ) {
          localStorage.removeItem(localStorageKey);

          // Put the column back in the correct position.
          this.columns_.splice(index, 0, axiom);

          // Force the view to update.
          this.columns_ = this.columns_.slice();
        } else {
          // The string isn't important, all that matters is that there's some
          // value for this key.
          localStorage.setItem(localStorageKey, 'Y');
          this.columns_ = this.columns_.filter((col) => col.name !== axiom.name);
        }
      };
    }
  ]
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.view',
  name: 'MultiChoiceView',
  extends: 'foam.u2.View',
  flags: ['web'],

  implements: [ 'foam.mlang.Expressions' ],

  documentation: `
    -takes a faceted CardSelectView - based on MinMax.choice[0].cls_

    Wraps a tag that represents multiple choices.

    The choices are in [value, label, isSelected, choiceMode ] quartets.

    However the client can simply pass in [value, label] and it will adapt to a [value, label, isSelected, choiceMode ] format

    Or the client can instead pass in a DAO to the MultiChoiceView.dao and the choices list will
    be automatically generated

    Calling the following methods:

    1. MultiChoiceView.outputSelectedChoicesInValueLabelFormat() - will return an array containing the
    selected choices in [ value, label ] format only if the minSelected, maxSelected criteria is respected
    2. MultiChoiceView.outputSelectedChoicesInValueLabelFormat() - will return a predicated dao containing thee
    selected choices in only if the minSelected, maxSelected criteria is respected

    MultiChoiceView.data will be automatically set to a predicated dao based on the choices selected only if
    the minSelected, maxSelected criteria is respected, it will be foam.dao.NullDAO othrewise

    this.booleanView is a ViewSpec for each choice. It defaults to foam.u2.CheckBox
  `,

  css: `
    ^helpTextRow {
      font-size: 14pt;
      padding: 8pt 0;
    }
    ^flexer {
      flex-wrap: wrap;
      align-items: stretch;
      text-align: center;
    }
    ^innerFlexer {
      display: inline-flex;
      padding: 4px;
      box-sizing: border-box;
    }
  `,

  messages: [
    { name: 'OPTIONS_MSG', message: 'options' },
    { name: 'CHOOSE_1_OF_FOLLOWING_OPTIONS', message: 'Choose one of the following options' },
    { name: 'CHOOSE_AT_LEAST_1_OPTION', message: 'Choose at least one option' },
    { name: 'CHOOSE_AT_LEAST', message: 'Choose at least' },
    { name: 'CHOOSE_EXACTLY', message: 'Choose exactly' },
    { name: 'CHOOSE', message: 'Choose' }
  ],

  properties: [
    {
      class: 'Function',
      name: 'onSelect'
    },
    {
      name: 'choices',
      documentation: `
        An array of choices which are single choice is denoted as [value, label, isSelected, choiceMode, isFinal], however the user can
        just pass in [value, label] and the adapt function will turn it into the [value, label, isSelected, choiceMode, isFinal] format
        for processing purposes
      `,
      factory: function() {
        return [];
      },
      adapt: function(_, n) {
        if ( ! Array.isArray(n) ) throw new Error('Please submit an array to choices in the MultiChoiceView');

        if ( n.length > 0 ) {
          n = n.sort();
          var valueLabelChoices = n.filter(choice => choice.length === 2)
          var fullChoices = n.filter(choice => choice.length === 5)

          if ( valueLabelChoices.length === n.length ) return n.map(choice => [ choice[0], choice[1], false, this.mode, false ])
          if ( fullChoices.length === n.length  ) return n;

          throw new Error('Items in choices array do not have consistent lengths')
        }

        return n;
      },
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao',
      documentation: `
        If the user just wants to pass in a dao, it will be processed to populate the
        choices array instead of manually inputing the choices
      `
    },
    {
      name: 'selectedChoices',
      value: [],
      postSet: function(o, n) {
        if ( this.onSelect ) this.onSelect(o, n);

        if ( this.selectedChoices.length < this.maxSelected ) {
          this.choices.forEach(choice => {
            var isSelected = foam.core.Slot.isInstance(choice[2])
              ? choice[2].get()
              : choice[2];

            var isFinal = foam.core.Slot.isInstance(choice[4])
              ? choice[4].get()
              : choice[4];

            if ( ! isSelected  && ! isFinal && foam.core.Slot.isInstance(choice[3]) ) {
              choice[3].set(foam.u2.DisplayMode.RW);
            }
          });
        } else {
          this.choices.forEach(choice => {
            var isSelected = foam.core.Slot.isInstance(choice[2])
              ? choice[2].get()
              : choice[2];

            var isFinal = foam.core.Slot.isInstance(choice[4])
              ? choice[4].get()
              : choice[4];

            if ( ! isSelected && ! isFinal && foam.core.Slot.isInstance(choice[3]) ) {
              choice[3].set(foam.u2.DisplayMode.DISABLED);
            }
          });
        }
      }
    },
    {
      class: 'Boolean',
      name: 'isValidNumberOfChoices',
      expression: function(selectedChoices, minSelected, maxSelected) {
        return selectedChoices.length >= minSelected && selectedChoices.length <= maxSelected;
      }
    },
    {
      class: 'Boolean',
      name: 'showMinMaxHelper',
      value: true
    },
    {
      class: 'Boolean',
      name: 'showValidNumberOfChoicesHelper',
      value: true
    },
    {
      class: 'Int',
      name: 'minSelected',
      expression: function(choices) {
        return choices.length > 0 ? 1 : 0;
      }
    },
    {
      class: 'Int',
      name: 'maxSelected',
      value: 2
    },
    {
      class: 'foam.u2.ViewSpecWithJava',
      name: 'booleanView',
      expression: function(choices) {
        var cap = choices
        && choices[0]
        && choices[0][2]
        && choices[0][2].obj
        && choices[0][2].obj.capability;
        var cls = cap.cls_.id;
        return { class: 'foam.u2.view.CardSelectView', of: cls, obj: cap };
      }
    },
    {
      class: 'Boolean',
      name: 'isVertical',
      value: false
    },
    {
      class: 'Boolean',
      name: 'isDaoFetched',
      value: false
    },
    {
      class: 'Int',
      name: 'numberOfColumns',
      value: 3
    },
    {
      name: 'objToChoice',
      class: 'Function',
      value: function(obj) {
        return [obj.id, obj.toSummary()];
      }
    },
    {
      name: 'helpText_',
      expression: function(minSelected, maxSelected) {
        // TODO: Change this when formatted messages are supported
        return ( maxSelected > 0 )
          ? ( minSelected == maxSelected )
            ? ( minSelected == 1 )
              ? this.CHOOSE_1_OF_FOLLOWING_OPTIONS
              : `${this.CHOOSE_EXACTLY} ${minSelected} ${this.OPTIONS_MSG}`
            : `${this.CHOOSE} ${minSelected} - ${maxSelected} ${this.OPTIONS_MSG}`
          : ( minSelected == 1 )
            ? this.CHOOSE_AT_LEAST_1_OPTION
            : `${this.CHOOSE_AT_LEAST} ${minSelected} ${this.OPTIONS_MSG}`
          ;
      }
    }
  ],

  methods: [
    function initE() {
      var self = this;

      this.onDAOUpdate();

      this
        .start()
          .add(self.slot(function(showMinMaxHelper, helpText_) {
            return self.E().callIf(showMinMaxHelper, function() {
              this
              .start(foam.u2.layout.Rows)
                .start()
                  .addClass(self.myClass('helpTextRow'))
                  .add(self.helpText_)
                .end()
              .end();
            });
          }))
        .end()
        .start(self.isVertical ? foam.u2.layout.Rows : foam.u2.layout.Cols)
          .addClass(self.myClass('flexer'))
          .add(
            self.isDaoFetched$.map(isDaoFetched => {
              var newChoices = [];

              var toRender = self.choices.map(function(choice) {
                var simpSlot0 = self.mustSlot(choice[0]);
                var simpSlot1 = self.mustSlot(choice[1]);
                var simpSlot2 = self.mustSlot(choice[2]);
                var simpSlot3 = self.mustSlot(choice[3]);
                var simpSlot4 = self.mustSlot(choice[4]);

                newChoices = [
                  ...newChoices,
                  [choice[0], simpSlot1, simpSlot2, simpSlot3, simpSlot4]
                ];

                return self.E()
                  .addClass(self.myClass('innerFlexer'))
                  // NOTE: This should not be the way we implement columns.
                  .style({
                    'width': `${100 / self.numberOfColumns}%`
                  })
                  .tag(self.booleanView, {
                      data$: simpSlot2,
                      label$: simpSlot1,
                      mode$: simpSlot3
                    });
              });

              self.selectedChoices$ = foam.core.ArraySlot.create({
                slots: newChoices.map(choice => {
                  return this.mustSlot(choice[2]);
                })
              }).map(v => {
                var selectedChoices = [];
                v.forEach((w, i) => {
                  if ( w ) {
                    selectedChoices.push(this.choices[i]);
                  }
                });
                return selectedChoices;
              });
              this.choices = newChoices;
              return toRender;
            })
          )
        .end()
        .start()
          .add(
            self.slot(function(showValidNumberOfChoicesHelper, isValidNumberOfChoices, choices) {
              return self.E()
              .callIf(! isValidNumberOfChoices && showValidNumberOfChoicesHelper, function() {
                this
                  .add('Please select a valid number of choices');
              })
              .callIf(isValidNumberOfChoices && showValidNumberOfChoicesHelper, function() {
                this
                  .start()
                    .add('Here are your valid selected choices using the outputSelectedChoicesInValueLabelFormat() function:')
                  .end()
                  .start()
                    .add(
                      self.outputSelectedChoicesInValueLabelFormat()
                        .map(function(choice) {
                          return self.E().add(`[${choice[0]},${choice[1]}]`);
                        })
                    )
                  .end();
              });
            })
          )
        .end();
    },

    function outputSelectedChoicesInValueLabelFormat() {
      if ( ! this.isValidNumberOfChoices ) {
        console.warn('Please select a valid number of choices');
        return [];
      }

      var filteredChoices = this.choices.filter(this.choiceTrue);

      return filteredChoices.map(choice => [choice[0], choice[1]]);
    },

    function outputSelectedChoicesInDAO() {
      if ( ! this.isValidNumberOfChoices ) {
        console.warn('Please select a valid number of choices');
        return foam.dao.NullDAO;
      }

      var of = this.dao.of;

      var filteredChoices = this.choices.filter(choice => choice[2]);

      return this.dao
        .where(this.IN(of.ID, filteredChoices.map(choice => choice[0])));
    },

    function choiceTrue(choice) {
      return foam.core.Slot.isInstance(choice[2])
        ? choice[2].get()
        : choice[2]
        ;
    },

    function mustSlot(v) {
      return foam.core.Slot.isInstance(v) ?
        v :
        foam.core.SimpleSlot.create({ value: v });
    }
  ],

  listeners: [
    {
      name: 'onDAOUpdate',
      isFramed: true,
      code: function() {
        if ( ! this.dao || ! foam.dao.DAO.isInstance(this.dao) ) return;

        var of = this.dao.of;
        if ( of._CHOICE_TEXT_ ) {
          this.dao.select(this.PROJECTION(of.ID, of._CHOICE_TEXT_)).then(s => {
            this.choices      = s.projection;
            this.isDaoFetched = true;
          });
          return;
        }
        console.warn('Inefficient ChoiceView. Consider creating transient _choiceText_ property on ' + of.id + ' DAO, prop: ' + this.prop_);

        /* Ex.:
        {
          class: 'String',
          name: '_choiceText_',
          transient: true,
          javaGetter: 'return getName();',
          getter: function() { return this.name; }
        }
        */
        var p = this.mode === foam.u2.DisplayMode.RW ?
          this.dao.select().then(s => s.array) :
          this.dao.find(this.data).then(o => o ? [o] : []);

        p.then(a => {
          var choices = a.map(this.objToChoice);
          var choiceLabels = a.map(o => this.objToChoice(o)[1]);
          Promise.all(choiceLabels).then(resolvedChoiceLabels => {
            for ( let i = 0; i < choices.length; i++ ) {
              choices[i][1] = resolvedChoiceLabels[i];
            }
            this.choices = choices;
            this.isDaoFetched = true;
          });
        });
      }
    }
  ]
});
<commit_msg>update MultiChoice- update cardview based on each cap & remove unnesseccary<commit_after>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.view',
  name: 'MultiChoiceView',
  extends: 'foam.u2.View',
  flags: ['web'],

  implements: [ 'foam.mlang.Expressions' ],

  requires: [ 'foam.u2.view.CardSelectView' ],

  documentation: `
    -takes a faceted CardSelectView - based on each choice[2].cls_

    Wraps a tag that represents multiple choices.

    The choices are in [value, label, isSelected, choiceMode ] quartets.

    However the client can simply pass in [value, label] and it will adapt to a [value, label, isSelected, choiceMode ] format

    Or the client can instead pass in a DAO to the MultiChoiceView.dao and the choices list will
    be automatically generated

    Calling the following methods:

    MultiChoiceView.data will be automatically set to a predicated dao based on the choices selected only if
    the minSelected, maxSelected criteria is respected, it will be foam.dao.NullDAO othrewise
  `,

  css: `
    ^helpTextRow {
      font-size: 14pt;
      padding: 8pt 0;
    }
    ^flexer {
      flex-wrap: wrap;
      align-items: stretch;
      text-align: center;
    }
    ^innerFlexer {
      display: inline-flex;
      padding: 4px;
      box-sizing: border-box;
    }
  `,

  constants: {
    NUM_COLUMNS: 3
  },

  messages: [
    { name: 'OPTIONS_MSG', message: 'options' },
    { name: 'CHOOSE_1_OF_FOLLOWING_OPTIONS', message: 'Choose one of the following options' },
    { name: 'CHOOSE_AT_LEAST_1_OPTION', message: 'Choose at least one option' },
    { name: 'CHOOSE_AT_LEAST', message: 'Choose at least' },
    { name: 'CHOOSE_EXACTLY', message: 'Choose exactly' },
    { name: 'CHOOSE', message: 'Choose' }
  ],

  properties: [
    {
      class: 'Function',
      name: 'onSelect'
    },
    {
      name: 'choices',
      documentation: `
        An array of choices which are single choice is denoted as [value, label, isSelected, choiceMode, isFinal], however the user can
        just pass in [value, label] and the adapt function will turn it into the [value, label, isSelected, choiceMode, isFinal] format
        for processing purposes
      `,
      factory: function() {
        return [];
      },
      adapt: function(_, n) {
        if ( ! Array.isArray(n) ) throw new Error('Please submit an array to choices in the MultiChoiceView');

        if ( n.length > 0 ) {
          n = n.sort();
          var valueLabelChoices = n.filter(choice => choice.length === 2);
          var fullChoices = n.filter(choice => choice.length === 5);

          if ( valueLabelChoices.length === n.length ) return n.map(choice => [choice[0], choice[1], false, this.mode, false]);
          if ( fullChoices.length === n.length ) return n;

          throw new Error('Items in choices array do not have consistent lengths');
        }

        return n;
      },
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao',
      documentation: `
        If the user just wants to pass in a dao, it will be processed to populate the
        choices array instead of manually inputing the choices
      `
    },
    {
      name: 'selectedChoices',
      value: [],
      postSet: function(o, n) {
        if ( this.onSelect ) this.onSelect(o, n);

        if ( this.selectedChoices.length < this.maxSelected ) {
          this.choices.forEach(choice => {
            var isSelected = foam.core.Slot.isInstance(choice[2])
              ? choice[2].get()
              : choice[2];

            var isFinal = foam.core.Slot.isInstance(choice[4])
              ? choice[4].get()
              : choice[4];

            if ( ! isSelected && ! isFinal && foam.core.Slot.isInstance(choice[3]) ) {
              choice[3].set(foam.u2.DisplayMode.RW);
            }
          });
        } else {
          this.choices.forEach(choice => {
            var isSelected = foam.core.Slot.isInstance(choice[2])
              ? choice[2].get()
              : choice[2];

            var isFinal = foam.core.Slot.isInstance(choice[4])
              ? choice[4].get()
              : choice[4];

            if ( ! isSelected && ! isFinal && foam.core.Slot.isInstance(choice[3]) ) {
              choice[3].set(foam.u2.DisplayMode.DISABLED);
            }
          });
        }
      }
    },
    {
      class: 'Boolean',
      name: 'showMinMaxHelper',
      value: true
    },
    {
      class: 'Int',
      name: 'minSelected',
      expression: function(choices) {
        return choices.length > 0 ? 1 : 0;
      }
    },
    {
      class: 'Int',
      name: 'maxSelected',
      value: 2
    },
    {
      class: 'Boolean',
      name: 'isVertical',
      value: false
    },
    {
      class: 'Boolean',
      name: 'isDaoFetched',
      value: false
    },
    {
      name: 'objToChoice',
      class: 'Function',
      value: function(obj) {
        return [obj.id, obj.toSummary()];
      }
    },
    {
      name: 'helpText_',
      expression: function(minSelected, maxSelected) {
        // TODO: Change this when formatted messages are supported
        return ( maxSelected > 0 )
          ? ( minSelected == maxSelected )
            ? ( minSelected == 1 )
              ? this.CHOOSE_1_OF_FOLLOWING_OPTIONS
              : `${this.CHOOSE_EXACTLY} ${minSelected} ${this.OPTIONS_MSG}`
            : `${this.CHOOSE} ${minSelected} - ${maxSelected} ${this.OPTIONS_MSG}`
          : ( minSelected == 1 )
            ? this.CHOOSE_AT_LEAST_1_OPTION
            : `${this.CHOOSE_AT_LEAST} ${minSelected} ${this.OPTIONS_MSG}`
          ;
      }
    }
  ],

  methods: [
    function initE() {
      var self = this;

      this.onDAOUpdate();

      this
        .start()
          .add(self.slot(function(showMinMaxHelper, helpText_) {
            return self.E().callIf(showMinMaxHelper, function() {
              this
              .start(foam.u2.layout.Rows)
                .start()
                  .addClass(self.myClass('helpTextRow'))
                  .add(self.helpText_)
                .end()
              .end();
            });
          }))
        .end()
        .start(self.isVertical ? foam.u2.layout.Rows : foam.u2.layout.Cols)
          .addClass(self.myClass('flexer'))
          .add( // TODO isDoaFetched and simpSlot0 aren't used should be clean up
            self.isDaoFetched$.map(isDaoFetched => {
              var newChoices = [];

              var toRender = self.choices.map(function(choice) {
                var simpSlot0 = self.mustSlot(choice[0]);
                var simpSlot1 = self.mustSlot(choice[1]);
                var simpSlot2 = self.mustSlot(choice[2]);
                var simpSlot3 = self.mustSlot(choice[3]);
                var simpSlot4 = self.mustSlot(choice[4]);

                newChoices = [
                  ...newChoices,
                  [choice[0], simpSlot1, simpSlot2, simpSlot3, simpSlot4]
                ];
                var cap = choice[2] && choice[2].obj && choice[2].obj.capability;
                var cls =  cap && cap.cls_.id;
                return self.E()
                  .addClass(self.myClass('innerFlexer'))
                  // NOTE: This should not be the way we implement columns.
                  .style({
                    'width': `${100 / self.NUM_COLUMNS}%`
                  })
                  .tag(self.CardSelectView, {
                      data$: simpSlot2,
                      label$: simpSlot1,
                      mode$: simpSlot3,
                      of: cls,
                      obj: cap
                    });
              });

              self.selectedChoices$ = foam.core.ArraySlot.create({
                slots: newChoices.map(choice => {
                  return this.mustSlot(choice[2]);
                })
              }).map(v => {
                var selectedChoices = [];
                v.forEach((w, i) => {
                  if ( w ) {
                    selectedChoices.push(this.choices[i]);
                  }
                });
                return selectedChoices;
              });
              this.choices = newChoices;
              return toRender;
            })
          )
        .end();
    },

    function mustSlot(v) {
      return foam.core.Slot.isInstance(v) ?
        v :
        foam.core.SimpleSlot.create({ value: v });
    }
  ],

  listeners: [
    {
      name: 'onDAOUpdate',
      isFramed: true,
      code: function() {
        if ( ! this.dao || ! foam.dao.DAO.isInstance(this.dao) ) return;

        var of = this.dao.of;
        if ( of._CHOICE_TEXT_ ) {
          this.dao.select(this.PROJECTION(of.ID, of._CHOICE_TEXT_)).then(s => {
            this.choices      = s.projection;
            this.isDaoFetched = true;
          });
          return;
        }
        console.warn('Inefficient ChoiceView. Consider creating transient _choiceText_ property on ' + of.id + ' DAO, prop: ' + this.prop_);

        /* Ex.:
        {
          class: 'String',
          name: '_choiceText_',
          transient: true,
          javaGetter: 'return getName();',
          getter: function() { return this.name; }
        }
        */
        var p = this.mode === foam.u2.DisplayMode.RW ?
          this.dao.select().then(s => s.array) :
          this.dao.find(this.data).then(o => o ? [o] : []);

        p.then(a => {
          var choices = a.map(this.objToChoice);
          var choiceLabels = a.map(o => this.objToChoice(o)[1]);
          Promise.all(choiceLabels).then(resolvedChoiceLabels => {
            for ( let i = 0; i < choices.length; i++ ) {
              choices[i][1] = resolvedChoiceLabels[i];
            }
            this.choices = choices;
            this.isDaoFetched = true;
          });
        });
      }
    }
  ]
});
<|endoftext|>


<commit_before>import React from 'react';
import cx from 'classnames';
import { props, t, skinnable } from '../utils';

@skinnable()
@props({
  /**
   * FocusableView content
   */
  children: t.union([t.ReactNode, t.Function]),
  /**
   *  Callback function called on "focus" event
   */
  onFocus: t.maybe(t.Function),
  /**
   *  Callback function called on "blur" event
   */
  onBlur: t.maybe(t.Function),
  /**
   *  "tabindex" attribute
   */
  tabIndex: t.maybe(t.Number),
  /**
   *  Wrapper component for `children`
   */
  component: t.maybe(t.union([t.Function, t.String])),
  /**
   *  When `true` the class "focused" is NOT added
   */
  ignoreFocus: t.maybe(t.Boolean),
  className: t.maybe(t.String),
  style: t.maybe(t.Object)
}, { strict: false })
export default class FocusableView extends React.Component {

  static defaultProps = {
    component: 'div',
    tabIndex: 0,
    onFocus: () => {},
    onBlur: () => {}
  };

  constructor(props) {
    super(props);
    this.state = { focused: false };
  }

  _onFocus = () => {
    this.setState({ focused: true });
    this.props.onFocus();
  };

  _onBlur = () => {
    this.setState({ focused: false });
    this.props.onBlur();
  };

  getLocals() {
    const { focused } = this.state;
    return {
      ...this.props,
      focused,
      className: !this.props.ignoreFocus ? cx(this.props.className, { focused }) : this.props.className,
      onFocus: this._onFocus,
      onBlur: this._onBlur
    };
  }

  template({ children, component, focused, ...locals }) {
    return React.createElement(
      component,
      locals,
      t.Function.is(children) ? children(focused) : children
    );
  }

}
<commit_msg>code clean<commit_after>import React from 'react';
import cx from 'classnames';
import { props, t, skinnable } from '../utils';

@skinnable()
@props({
  /**
   * FocusableView content. If a function it gets called with the boolean "focused".
   */
  children: t.union([t.ReactNode, t.Function]),
  /**
   *  Callback function called on "focus" event
   */
  onFocus: t.maybe(t.Function),
  /**
   *  Callback function called on "blur" event
   */
  onBlur: t.maybe(t.Function),
  /**
   *  "tabindex" attribute
   */
  tabIndex: t.maybe(t.Number),
  /**
   *  Wrapper component for `children`
   */
  component: t.maybe(t.union([t.Function, t.String])),
  /**
   *  When `true` the class "focused" is NOT added
   */
  ignoreFocus: t.maybe(t.Boolean),
  className: t.maybe(t.String),
  style: t.maybe(t.Object)
}, { strict: false })
export default class FocusableView extends React.Component {

  static defaultProps = {
    component: 'div',
    tabIndex: 0,
    onFocus: () => {},
    onBlur: () => {}
  };

  constructor(props) {
    super(props);
    this.state = { focused: false };
  }

  _onFocus = () => {
    this.setState({ focused: true });
    this.props.onFocus();
  };

  _onBlur = () => {
    this.setState({ focused: false });
    this.props.onBlur();
  };

  getLocals() {
    const { focused } = this.state;
    const { className, ignoreFocus, ...props } = this.props;
    return {
      ...props,
      focused,
      className: !ignoreFocus ? cx(className, { focused }) : className,
      onFocus: this._onFocus,
      onBlur: this._onBlur
    };
  }

  template({ children, component, focused, ...locals }) {
    return React.createElement(
      component,
      locals,
      t.Function.is(children) ? children(focused) : children
    );
  }

}
<|endoftext|>