<commit_before>/* jshint node: true */
'use strict';

var mbus = require('mbus');
var queue = require('rtc-taskqueue');
var cleanup = require('./cleanup');
var monitor = require('./monitor');
var throttle = require('cog/throttle');
var pluck = require('whisk/pluck');
var pluckCandidate = pluck('candidate', 'sdpMid', 'sdpMLineIndex');
var CLOSED_STATES = [ 'closed', 'failed' ];
var CHECKING_STATES = [ 'checking' ];

/**
  ### rtc-tools/couple

  #### couple(pc, targetId, signaller, opts?)

  Couple a WebRTC connection with another webrtc connection identified by
  `targetId` via the signaller.

  The following options can be provided in the `opts` argument:

  - `sdpfilter` (default: null)

    A simple function for filtering SDP as part of the peer
    connection handshake (see the Using Filters details below).

  ##### Example Usage

  ```js
  var couple = require('rtc/couple');

  couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller);
  ```

  ##### Using Filters

  In certain instances you may wish to modify the raw SDP that is provided
  by the `createOffer` and `createAnswer` calls.  This can be done by passing
  a `sdpfilter` function (or array) in the options.  For example:

  ```js
  // run the sdp from through a local tweakSdp function.
  couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller, {
    sdpfilter: tweakSdp
  });
  ```

**/
function couple(pc, targetId, signaller, opts) {
  var debugLabel = (opts || {}).debugLabel || 'rtc';
  var debug = require('cog/logger')(debugLabel + '/couple');

  // create a monitor for the connection
  var mon = monitor(pc, targetId, signaller, (opts || {}).logger);
  var emit = mbus('', mon);
  var reactive = (opts || {}).reactive;
  var endOfCandidates = true;

  // configure the time to wait between receiving a 'disconnect'
  // iceConnectionState and determining that we are closed
  var disconnectTimeout = (opts || {}).disconnectTimeout || 10000;
  var disconnectTimer;

  // Target ready indicates that the target peer has indicated it is
  // ready to begin coupling
  var targetReady = false;
  var targetInfo = undefined;
  var readyInterval = (opts || {}).readyInterval || 100;
  var readyTimer;

  // Failure timeout
  var failTimeout = (opts || {}).failTimeout || 30000;
  var failTimer;

  // Request offer timer
  var requestOfferTimer;

  // Interoperability flags
  var allowReactiveInterop = (opts || {}).allowReactiveInterop;

  // initilaise the negotiation helpers
  var isMaster = signaller.isMaster(targetId);

  // initialise the processing queue (one at a time please)
  var q = queue(pc, opts);
  var coupling = false;
  var negotiationRequired = false;
  var renegotiateRequired = false;
  var creatingOffer = false;
  var interoperating = false;

  /**
    Indicates whether this peer connection is in a state where it is able to have new offers created
   **/
  function isReadyForOffer() {
    return !coupling && pc.signalingState === 'stable';
  }

  function createOffer() {
    // If coupling is already in progress, return
    if (!isReadyForOffer()) return;

    debug('[' + signaller.id + '] ' + 'Creating new offer for connection to ' + targetId);
    // Otherwise, create the offer
    coupling = true;
    creatingOffer = true;
    negotiationRequired = false;
    q.createOffer().then(function() {
      creatingOffer = false;
    }).catch(function() {
      creatingOffer = false;
    });
  }

  var createOrRequestOffer = throttle(function() {
    if (!targetReady) {
      debug('[' + signaller.id + '] ' + targetId + ' not yet ready for offer');
      return emit.once('target.ready', createOrRequestOffer);
    }

    // If this is not the master, always send the negotiate request
    // Redundant requests are eliminated on the master side
    if (! isMaster) {
      debug('[' + signaller.id + '] ' + 'Requesting negotiation from ' + targetId + ' (requesting offerer? ' + renegotiateRequired + ')');
      // Due to https://bugs.chromium.org/p/webrtc/issues/detail?id=2782 which involves incompatibilities between
      // Chrome and Firefox created offers by default client offers are disabled to ensure that all offers are coming
      // from the same source. By passing `allowReactiveInterop` you can reallow this, then use the `filtersdp` option
      // to provide a munged SDP that might be able to work
      return signaller.to(targetId).send('/negotiate', {
        requestOfferer: (allowReactiveInterop || !interoperating) && renegotiateRequired
      });
    }

    return createOffer();
  }, 100, { leading: false });

  function decouple() {
    debug('decoupling ' + signaller.id + ' from ' + targetId);

    // Clear any outstanding timers
    clearTimeout(readyTimer);
    clearTimeout(disconnectTimer);
    clearTimeout(requestOfferTimer);
    clearTimeout(failTimer);

    // stop the monitor
//     mon.removeAllListeners();
    mon.close();

    // cleanup the peerconnection
    cleanup(pc);

    // remove listeners
    signaller.removeListener('sdp', handleSdp);
    signaller.removeListener('candidate', handleCandidate);
    signaller.removeListener('endofcandidates', handleLastCandidate);
    signaller.removeListener('negotiate', handleNegotiateRequest);
    signaller.removeListener('ready', handleReady);
    signaller.removeListener('requestoffer', handleRequestOffer);

    // remove listeners (version >= 5)
    signaller.removeListener('message:sdp', handleSdp);
    signaller.removeListener('message:candidate', handleCandidate);
    signaller.removeListener('message:endofcandidates', handleLastCandidate);
    signaller.removeListener('message:negotiate', handleNegotiateRequest);
    signaller.removeListener('message:ready', handleReady);
    signaller.removeListener('message:requestoffer', handleRequestOffer);

  }

  function handleCandidate(data, src) {
    // if the source is unknown or not a match, then don't process
    if ((! src) || (src.id !== targetId)) {
      return;
    }

    q.addIceCandidate(data);
  }

  // No op
  function handleLastCandidate() {
  }

  function handleSdp(sdp, src) {
    // if the source is unknown or not a match, then don't process
    if ((! src) || (src.id !== targetId)) {
      return;
    }

    emit('sdp.remote', sdp);

    // To speed up things on the renegotiation side of things, determine whether we have
    // finished the coupling (offer -> answer) cycle, and whether it is safe to start
    // renegotiating prior to the iceConnectionState "completed" state
    q.setRemoteDescription(sdp).then(function() {

      // If this is the peer that is coupling, and we have received the answer so we can
      // and assume that coupling (offer -> answer) process is complete, so we can clear the coupling flag
      if (coupling && sdp.type === 'answer') {
        debug('coupling complete, can now trigger any pending renegotiations');
        if (isMaster && negotiationRequired) createOrRequestOffer();
      }
    });
  }

  function handleReady(src) {
    if (targetReady || !src || src.id !== targetId) {
      return;
    }
    debug('[' + signaller.id + '] ' + targetId + ' is ready for coupling');
    targetReady = true;
    targetInfo = src.data;
    interoperating = (targetInfo.browser !== signaller.attributes.browser);
    emit('target.ready');
  }

  function handleConnectionClose() {
    debug('captured pc close, iceConnectionState = ' + pc.iceConnectionState);
    decouple();
  }

  function handleDisconnect() {
    debug('captured pc disconnect, monitoring connection status');

    // start the disconnect timer
    disconnectTimer = setTimeout(function() {
      debug('manually closing connection after disconnect timeout');
      mon('failed');
      cleanup(pc);
    }, disconnectTimeout);

    mon.on('statechange', handleDisconnectAbort);
    mon('failing');
  }

  function handleDisconnectAbort() {
    debug('connection state changed to: ' + pc.iceConnectionState);

    // if the state is checking, then do not reset the disconnect timer as
    // we are doing our own checking
    if (CHECKING_STATES.indexOf(pc.iceConnectionState) >= 0) {
      return;
    }

    resetDisconnectTimer();

    // if we have a closed or failed status, then close the connection
    if (CLOSED_STATES.indexOf(pc.iceConnectionState) >= 0) {
      return mon('closed');
    }

    mon.once('disconnect', handleDisconnect);
  }

  function handleLocalCandidate(evt) {
    var data = evt.candidate && pluckCandidate(evt.candidate);

    if (evt.candidate) {
      resetDisconnectTimer();
      emit('ice.local', data);
      signaller.to(targetId).send('/candidate', data);
      endOfCandidates = false;
    }
    else if (! endOfCandidates) {
      endOfCandidates = true;
      emit('ice.gathercomplete');
      signaller.to(targetId).send('/endofcandidates', {});
    }
  }

  function requestNegotiation() {
    // This is a redundant request if not reactive
    if (coupling && !reactive) return;

    // If no coupling is occurring, regardless of reactive, start the offer process
    if (!coupling) return createOrRequestOffer();

    // If we are already coupling, we are reactive and renegotiation has not been indicated
    // defer a negotiation request
    if (coupling && reactive && !negotiationRequired) {
      debug('renegotiation is required, but deferring until existing connection is established');
      negotiationRequired = true;

      // NOTE: This is commented out, as the functionality after the setRemoteDescription
      // should adequately take care of this. But should it not, re-enable this
      // mon.once('connectionstate:completed', function() {
      //   createOrRequestOffer();
      // });
    }
  }

  /**
    This allows the master to request the client to send an offer
   **/
  function requestOfferFromClient() {
    if (requestOfferTimer) clearTimeout(requestOfferTimer);
    if (pc.signalingState === 'closed') return;

    // Check if we are ready for a new offer, otherwise delay
    if (!isReadyForOffer()) {
      debug('[' + signaller.id + '] negotiation request denied, not in a state to accept new offers [coupling = ' + coupling + ', ' + pc.signalingState + ']');
      requestOfferTimer = setTimeout(requestOfferFromClient, 500);
    } else {
       // Flag as coupling and request the client send the offer
      debug('[' + signaller.id + '] ' + targetId + ' has requested the ability to create the offer');
      coupling = true;
      return signaller.to(targetId).send('/requestoffer');
    }
  }

  function handleNegotiateRequest(data, src) {
    debug('[' + signaller.id + '] ' + src.id + ' has requested a negotiation');

    // Sanity check that this is for the target
    if (!src || src.id !== targetId) return;
    emit('negotiate.request', src.id, data);

    // Check if the client is requesting the ability to create the offer themselves
    if (data && data.requestOfferer) {
      return requestOfferFromClient();
    }

    // Otherwise, begin the traditional master driven negotiation process
    requestNegotiation();
  }

  function handleRenegotiateRequest() {
    if (!reactive) return;
    emit('negotiate.renegotiate');
    renegotiateRequired = true;
    requestNegotiation();
  }

  function resetDisconnectTimer() {
    var recovered = !!disconnectTimer && CLOSED_STATES.indexOf(pc.iceConnectionState) === -1;
    mon.off('statechange', handleDisconnectAbort);

    // clear the disconnect timer
    debug('reset disconnect timer, state: ' + pc.iceConnectionState);
    clearTimeout(disconnectTimer);
    disconnectTimer = undefined;

    // Trigger the recovered event if this is a recovery
    if (recovered) {
      mon('recovered');
    }
  }

  /**
    Allow clients to send offers
   **/
  function handleRequestOffer(src) {
    debug('[' + signaller.id + '] ' + targetId + ' has requested that the offer be sent');
    return createOffer();
  }

  // when regotiation is needed look for the peer
  if (reactive) {
    pc.onnegotiationneeded = handleRenegotiateRequest;
  }

  pc.onicecandidate = handleLocalCandidate;

  // when the task queue tells us we have sdp available, send that over the wire
  q.on('sdp.local', function(desc) {
    signaller.to(targetId).send('/sdp', desc);
  });

  // when we receive sdp, then
  signaller.on('sdp', handleSdp);
  signaller.on('candidate', handleCandidate);
  signaller.on('endofcandidates', handleLastCandidate);
  signaller.on('ready', handleReady);

  // listeners (signaller >= 5)
  signaller.on('message:sdp', handleSdp);
  signaller.on('message:candidate', handleCandidate);
  signaller.on('message:endofcandidates', handleLastCandidate);
  signaller.on('message:ready', handleReady);

  // if this is a master connection, listen for negotiate events
  if (isMaster) {
    signaller.on('negotiate', handleNegotiateRequest);
    signaller.on('message:negotiate', handleNegotiateRequest); // signaller >= 5
  } else {
    signaller.on('requestoffer', handleRequestOffer);
    signaller.on('message:requestoffer', handleRequestOffer);
  }

  // when the connection closes, remove event handlers
  mon.once('closed', handleConnectionClose);
  mon.once('disconnected', handleDisconnect);

  // patch in the create offer functions
  mon.createOffer = createOrRequestOffer;

  // A heavy handed approach to ensuring readiness across the coupling
  // peers. Will periodically send the `ready` message to the target peer
  // until the target peer has acknowledged that it also is ready - at which
  // point the offer can be sent
  function checkReady() {
    clearTimeout(readyTimer);
    signaller.to(targetId).send('/ready');

    // If we are ready, they've told us they are ready, and we've told
    // them we're ready, then exit
    if (targetReady) return;

    // Otherwise, keep telling them we're ready
    readyTimer = setTimeout(checkReady, readyInterval);
  }
  checkReady();
  debug('[' + signaller.id + '] ready for coupling to ' + targetId);

  // If we fail to connect within the given timeframe, trigger a failure
  failTimer = setTimeout(function() {
    mon('failed');
    decouple();
  }, failTimeout);

  mon.once('connected', function() {
    clearTimeout(failTimer);
  });

  mon.on('signalingchange', function(pc, state) {
    debug('[' + signaller.id + '] signaling state ' + state + ' to ' + targetId);
  });

  mon.on('signaling:stable', function() {
    // Check if the coupling process is over
    // creatingOffer is required due to the delay between the creation of the offer and the signaling
    // state changing to have-local-offer
    if (!creatingOffer && coupling) coupling = false;

    // Check if we have any pending negotiations
    if (negotiationRequired) {
      createOrRequestOffer();
    }
  });

  mon.stop = decouple;

  /**
    Aborts the coupling process
   **/
  mon.abort = function() {
    if (failTimer) {
      clearTimeout(failTimer);
    }
    decouple();
    mon('aborted');
  };

  return mon;
}

module.exports = couple;<commit_msg>Only send the offer is requested by the intended target<commit_after>/* jshint node: true */
'use strict';

var mbus = require('mbus');
var queue = require('rtc-taskqueue');
var cleanup = require('./cleanup');
var monitor = require('./monitor');
var throttle = require('cog/throttle');
var pluck = require('whisk/pluck');
var pluckCandidate = pluck('candidate', 'sdpMid', 'sdpMLineIndex');
var CLOSED_STATES = [ 'closed', 'failed' ];
var CHECKING_STATES = [ 'checking' ];

/**
  ### rtc-tools/couple

  #### couple(pc, targetId, signaller, opts?)

  Couple a WebRTC connection with another webrtc connection identified by
  `targetId` via the signaller.

  The following options can be provided in the `opts` argument:

  - `sdpfilter` (default: null)

    A simple function for filtering SDP as part of the peer
    connection handshake (see the Using Filters details below).

  ##### Example Usage

  ```js
  var couple = require('rtc/couple');

  couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller);
  ```

  ##### Using Filters

  In certain instances you may wish to modify the raw SDP that is provided
  by the `createOffer` and `createAnswer` calls.  This can be done by passing
  a `sdpfilter` function (or array) in the options.  For example:

  ```js
  // run the sdp from through a local tweakSdp function.
  couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller, {
    sdpfilter: tweakSdp
  });
  ```

**/
function couple(pc, targetId, signaller, opts) {
  var debugLabel = (opts || {}).debugLabel || 'rtc';
  var debug = require('cog/logger')(debugLabel + '/couple');

  // create a monitor for the connection
  var mon = monitor(pc, targetId, signaller, (opts || {}).logger);
  var emit = mbus('', mon);
  var reactive = (opts || {}).reactive;
  var endOfCandidates = true;

  // configure the time to wait between receiving a 'disconnect'
  // iceConnectionState and determining that we are closed
  var disconnectTimeout = (opts || {}).disconnectTimeout || 10000;
  var disconnectTimer;

  // Target ready indicates that the target peer has indicated it is
  // ready to begin coupling
  var targetReady = false;
  var targetInfo = undefined;
  var readyInterval = (opts || {}).readyInterval || 100;
  var readyTimer;

  // Failure timeout
  var failTimeout = (opts || {}).failTimeout || 30000;
  var failTimer;

  // Request offer timer
  var requestOfferTimer;

  // Interoperability flags
  var allowReactiveInterop = (opts || {}).allowReactiveInterop;

  // initilaise the negotiation helpers
  var isMaster = signaller.isMaster(targetId);

  // initialise the processing queue (one at a time please)
  var q = queue(pc, opts);
  var coupling = false;
  var negotiationRequired = false;
  var renegotiateRequired = false;
  var creatingOffer = false;
  var interoperating = false;

  /**
    Indicates whether this peer connection is in a state where it is able to have new offers created
   **/
  function isReadyForOffer() {
    return !coupling && pc.signalingState === 'stable';
  }

  function createOffer() {
    // If coupling is already in progress, return
    if (!isReadyForOffer()) return;

    debug('[' + signaller.id + '] ' + 'Creating new offer for connection to ' + targetId);
    // Otherwise, create the offer
    coupling = true;
    creatingOffer = true;
    negotiationRequired = false;
    q.createOffer().then(function() {
      creatingOffer = false;
    }).catch(function() {
      creatingOffer = false;
    });
  }

  var createOrRequestOffer = throttle(function() {
    if (!targetReady) {
      debug('[' + signaller.id + '] ' + targetId + ' not yet ready for offer');
      return emit.once('target.ready', createOrRequestOffer);
    }

    // If this is not the master, always send the negotiate request
    // Redundant requests are eliminated on the master side
    if (! isMaster) {
      debug('[' + signaller.id + '] ' + 'Requesting negotiation from ' + targetId + ' (requesting offerer? ' + renegotiateRequired + ')');
      // Due to https://bugs.chromium.org/p/webrtc/issues/detail?id=2782 which involves incompatibilities between
      // Chrome and Firefox created offers by default client offers are disabled to ensure that all offers are coming
      // from the same source. By passing `allowReactiveInterop` you can reallow this, then use the `filtersdp` option
      // to provide a munged SDP that might be able to work
      return signaller.to(targetId).send('/negotiate', {
        requestOfferer: (allowReactiveInterop || !interoperating) && renegotiateRequired
      });
    }

    return createOffer();
  }, 100, { leading: false });

  function decouple() {
    debug('decoupling ' + signaller.id + ' from ' + targetId);

    // Clear any outstanding timers
    clearTimeout(readyTimer);
    clearTimeout(disconnectTimer);
    clearTimeout(requestOfferTimer);
    clearTimeout(failTimer);

    // stop the monitor
//     mon.removeAllListeners();
    mon.close();

    // cleanup the peerconnection
    cleanup(pc);

    // remove listeners
    signaller.removeListener('sdp', handleSdp);
    signaller.removeListener('candidate', handleCandidate);
    signaller.removeListener('endofcandidates', handleLastCandidate);
    signaller.removeListener('negotiate', handleNegotiateRequest);
    signaller.removeListener('ready', handleReady);
    signaller.removeListener('requestoffer', handleRequestOffer);

    // remove listeners (version >= 5)
    signaller.removeListener('message:sdp', handleSdp);
    signaller.removeListener('message:candidate', handleCandidate);
    signaller.removeListener('message:endofcandidates', handleLastCandidate);
    signaller.removeListener('message:negotiate', handleNegotiateRequest);
    signaller.removeListener('message:ready', handleReady);
    signaller.removeListener('message:requestoffer', handleRequestOffer);

  }

  function handleCandidate(data, src) {
    // if the source is unknown or not a match, then don't process
    if ((! src) || (src.id !== targetId)) {
      return;
    }

    q.addIceCandidate(data);
  }

  // No op
  function handleLastCandidate() {
  }

  function handleSdp(sdp, src) {
    // if the source is unknown or not a match, then don't process
    if ((! src) || (src.id !== targetId)) {
      return;
    }

    emit('sdp.remote', sdp);

    // To speed up things on the renegotiation side of things, determine whether we have
    // finished the coupling (offer -> answer) cycle, and whether it is safe to start
    // renegotiating prior to the iceConnectionState "completed" state
    q.setRemoteDescription(sdp).then(function() {

      // If this is the peer that is coupling, and we have received the answer so we can
      // and assume that coupling (offer -> answer) process is complete, so we can clear the coupling flag
      if (coupling && sdp.type === 'answer') {
        debug('coupling complete, can now trigger any pending renegotiations');
        if (isMaster && negotiationRequired) createOrRequestOffer();
      }
    });
  }

  function handleReady(src) {
    if (targetReady || !src || src.id !== targetId) {
      return;
    }
    debug('[' + signaller.id + '] ' + targetId + ' is ready for coupling');
    targetReady = true;
    targetInfo = src.data;
    interoperating = (targetInfo.browser !== signaller.attributes.browser);
    emit('target.ready');
  }

  function handleConnectionClose() {
    debug('captured pc close, iceConnectionState = ' + pc.iceConnectionState);
    decouple();
  }

  function handleDisconnect() {
    debug('captured pc disconnect, monitoring connection status');

    // start the disconnect timer
    disconnectTimer = setTimeout(function() {
      debug('manually closing connection after disconnect timeout');
      mon('failed');
      cleanup(pc);
    }, disconnectTimeout);

    mon.on('statechange', handleDisconnectAbort);
    mon('failing');
  }

  function handleDisconnectAbort() {
    debug('connection state changed to: ' + pc.iceConnectionState);

    // if the state is checking, then do not reset the disconnect timer as
    // we are doing our own checking
    if (CHECKING_STATES.indexOf(pc.iceConnectionState) >= 0) {
      return;
    }

    resetDisconnectTimer();

    // if we have a closed or failed status, then close the connection
    if (CLOSED_STATES.indexOf(pc.iceConnectionState) >= 0) {
      return mon('closed');
    }

    mon.once('disconnect', handleDisconnect);
  }

  function handleLocalCandidate(evt) {
    var data = evt.candidate && pluckCandidate(evt.candidate);

    if (evt.candidate) {
      resetDisconnectTimer();
      emit('ice.local', data);
      signaller.to(targetId).send('/candidate', data);
      endOfCandidates = false;
    }
    else if (! endOfCandidates) {
      endOfCandidates = true;
      emit('ice.gathercomplete');
      signaller.to(targetId).send('/endofcandidates', {});
    }
  }

  function requestNegotiation() {
    // This is a redundant request if not reactive
    if (coupling && !reactive) return;

    // If no coupling is occurring, regardless of reactive, start the offer process
    if (!coupling) return createOrRequestOffer();

    // If we are already coupling, we are reactive and renegotiation has not been indicated
    // defer a negotiation request
    if (coupling && reactive && !negotiationRequired) {
      debug('renegotiation is required, but deferring until existing connection is established');
      negotiationRequired = true;

      // NOTE: This is commented out, as the functionality after the setRemoteDescription
      // should adequately take care of this. But should it not, re-enable this
      // mon.once('connectionstate:completed', function() {
      //   createOrRequestOffer();
      // });
    }
  }

  /**
    This allows the master to request the client to send an offer
   **/
  function requestOfferFromClient() {
    if (requestOfferTimer) clearTimeout(requestOfferTimer);
    if (pc.signalingState === 'closed') return;

    // Check if we are ready for a new offer, otherwise delay
    if (!isReadyForOffer()) {
      debug('[' + signaller.id + '] negotiation request denied, not in a state to accept new offers [coupling = ' + coupling + ', ' + pc.signalingState + ']');
      requestOfferTimer = setTimeout(requestOfferFromClient, 500);
    } else {
       // Flag as coupling and request the client send the offer
      debug('[' + signaller.id + '] ' + targetId + ' has requested the ability to create the offer');
      coupling = true;
      return signaller.to(targetId).send('/requestoffer');
    }
  }

  function handleNegotiateRequest(data, src) {
    debug('[' + signaller.id + '] ' + src.id + ' has requested a negotiation');

    // Sanity check that this is for the target
    if (!src || src.id !== targetId) return;
    emit('negotiate.request', src.id, data);

    // Check if the client is requesting the ability to create the offer themselves
    if (data && data.requestOfferer) {
      return requestOfferFromClient();
    }

    // Otherwise, begin the traditional master driven negotiation process
    requestNegotiation();
  }

  function handleRenegotiateRequest() {
    if (!reactive) return;
    emit('negotiate.renegotiate');
    renegotiateRequired = true;
    requestNegotiation();
  }

  function resetDisconnectTimer() {
    var recovered = !!disconnectTimer && CLOSED_STATES.indexOf(pc.iceConnectionState) === -1;
    mon.off('statechange', handleDisconnectAbort);

    // clear the disconnect timer
    debug('reset disconnect timer, state: ' + pc.iceConnectionState);
    clearTimeout(disconnectTimer);
    disconnectTimer = undefined;

    // Trigger the recovered event if this is a recovery
    if (recovered) {
      mon('recovered');
    }
  }

  /**
    Allow clients to send offers
   **/
  function handleRequestOffer(src) {
    if (!src || src.id !== targetId) return;
    debug('[' + signaller.id + '] ' + targetId + ' has requested that the offer be sent [' + src.id + ']');
    return createOffer();
  }

  // when regotiation is needed look for the peer
  if (reactive) {
    pc.onnegotiationneeded = handleRenegotiateRequest;
  }

  pc.onicecandidate = handleLocalCandidate;

  // when the task queue tells us we have sdp available, send that over the wire
  q.on('sdp.local', function(desc) {
    signaller.to(targetId).send('/sdp', desc);
  });

  // when we receive sdp, then
  signaller.on('sdp', handleSdp);
  signaller.on('candidate', handleCandidate);
  signaller.on('endofcandidates', handleLastCandidate);
  signaller.on('ready', handleReady);

  // listeners (signaller >= 5)
  signaller.on('message:sdp', handleSdp);
  signaller.on('message:candidate', handleCandidate);
  signaller.on('message:endofcandidates', handleLastCandidate);
  signaller.on('message:ready', handleReady);

  // if this is a master connection, listen for negotiate events
  if (isMaster) {
    signaller.on('negotiate', handleNegotiateRequest);
    signaller.on('message:negotiate', handleNegotiateRequest); // signaller >= 5
  } else {
    signaller.on('requestoffer', handleRequestOffer);
    signaller.on('message:requestoffer', handleRequestOffer);
  }

  // when the connection closes, remove event handlers
  mon.once('closed', handleConnectionClose);
  mon.once('disconnected', handleDisconnect);

  // patch in the create offer functions
  mon.createOffer = createOrRequestOffer;

  // A heavy handed approach to ensuring readiness across the coupling
  // peers. Will periodically send the `ready` message to the target peer
  // until the target peer has acknowledged that it also is ready - at which
  // point the offer can be sent
  function checkReady() {
    clearTimeout(readyTimer);
    signaller.to(targetId).send('/ready');

    // If we are ready, they've told us they are ready, and we've told
    // them we're ready, then exit
    if (targetReady) return;

    // Otherwise, keep telling them we're ready
    readyTimer = setTimeout(checkReady, readyInterval);
  }
  checkReady();
  debug('[' + signaller.id + '] ready for coupling to ' + targetId);

  // If we fail to connect within the given timeframe, trigger a failure
  failTimer = setTimeout(function() {
    mon('failed');
    decouple();
  }, failTimeout);

  mon.once('connected', function() {
    clearTimeout(failTimer);
  });

  mon.on('signalingchange', function(pc, state) {
    debug('[' + signaller.id + '] signaling state ' + state + ' to ' + targetId);
  });

  mon.on('signaling:stable', function() {
    // Check if the coupling process is over
    // creatingOffer is required due to the delay between the creation of the offer and the signaling
    // state changing to have-local-offer
    if (!creatingOffer && coupling) coupling = false;

    // Check if we have any pending negotiations
    if (negotiationRequired) {
      createOrRequestOffer();
    }
  });

  mon.stop = decouple;

  /**
    Aborts the coupling process
   **/
  mon.abort = function() {
    if (failTimer) {
      clearTimeout(failTimer);
    }
    decouple();
    mon('aborted');
  };

  return mon;
}

module.exports = couple;<|endoftext|>


<commit_before><commit_msg>Deprecate copying lesson cover<commit_after><|endoftext|>


<commit_before>//<!--  Contains functions to create, draw and modify creeps -->

var creeps = [];

function creep(posX,posY,direction,hitPoints){
	this.posX = posX;
	this.posY = posY;
	this.direction = direction;
	this.hitPoints = hitPoints;
	this.size = 10;
	this.image = new Image();
	this.image.src = imgIndifferentSmily;
	this.randomShort = 5;
	this.randomMedium = 5;
	this.randomLong = 5;
	this.deadCycle = undefined;
	this.drawSize = 50;

	this.drawCreep = drawCreep;
	function drawCreep(ctx){
		ctx.save();
	  ctx.translate(this.posX, this.posY);
	  ctx.rotate(to_rad(this.direction));
		ctx.drawImage(this.image,(this.drawSize/-2),(this.drawSize/-2), this.drawSize, this.drawSize);
	  ctx.restore();
	};
};

function drawAllCreeps(ctx){
	for (var i = 0; i < creeps.length; i++) {
    if (creeps[i] != undefined) {
      creeps[i].drawCreep(ctx);
    };
  };
};

function updateCreeps(){

	if (countCollection(creeps) < 11){
		createCreep(400,400);
	};

	console.log(countCollection(creeps));

	for (var i = 0; i < creeps.length; i++) {
    if (creeps[i] != undefined) {

	    creeps[i].direction = creeps[i].direction + ((creeps[i].randomMedium - 5) * .1);
	    creeps[i].posX = creeps[i].posX + ((creeps[i].randomShort - 5) * .5);
	    creeps[i].posY = creeps[i].posY + ((creeps[i].randomMedium - 5) * .3);

	    if (Math.abs(creeps[i].posX - 500) > 1000 || Math.abs(creeps[i].posY - 400)  > 800) {
	    	creeps[i].hitPoints = 0;
	    };

	    if (creeps[i].hitPoints < 1) {
	    	if (creeps[i].deadCycle == undefined) {
	    		creeps[i].deadCycle = gameLoopCounter;
	    	};
	    	deathRattle(creeps[i].posX, creeps[i].posY, i);
	    	if (creeps[i].deadCycle < (gameLoopCounter - 0)) {
	    		creeps[i] = undefined;
	    	};
	    };

	  };
	};
};

function hitCreep(i){
	creeps[towers[i].targetCreep].image.src = imgUnicorn;
	creeps[towers[i].targetCreep].drawSize = 100;
	creeps[towers[i].targetCreep].hitPoints = creeps[towers[i].targetCreep].hitPoints - 10;
};

function createCreep(x,y){
		console.log("create Creep!")
		creeps.push(new creep(x,y,0,20));
	}
};

function amIHit(creep,posX,posY,size){
	return false; // temporary!
};

function deathRattle(posX, posY, creepIndex){
	// alert("deathRattle - a creep has died")
	// console.log("deathRattle called")
};

function updateRandomFactors(collection){

		if (gameLoopCounter%30 == 1){	// SHORT random factor generator
		console.log("%30")
		for (var i = 0; i < creeps.length; i++) {
		    creeps[i].randomShort = to_i(((Math.random() * 10) + 1));
		    console.log(creeps[i].randomLong)
		};
	};

	if (gameLoopCounter%100 == 1){	// MEDIUM random factor generator
		console.log("%100")
		for (var i = 0; i < creeps.length; i++) {
		    creeps[i].randomMedium = to_i(((Math.random() * 10) + 1));
		    console.log(creeps[i].randomMedium)
		};
	};

	if (gameLoopCounter%1000 == 1){	// LONG random factor generator
		console.log("%1000")
		for (var i = 0; i < creeps.length; i++) {
		    creeps[i].randomLong = to_i(((Math.random() * 10) + 1));
		    console.log(creeps[i].randomLong)
		};
	};
}

// DRIVER CODE FOR DEV

// var my_creep = new creep(220,220,0,10);
// var my_creep2 = new creep(300,500,0,10);
// var my_creep3 = new creep(400,300,0,10);
// creeps.push(my_creep);
// creeps.push(my_creep3);
// creeps.push(my_creep2);

// var my_creep = new creep(200,200,0,10);
// var my_creep2 = new creep(400,500,0,10);
// var my_creep3 = new creep(200,500,0,10);
// creeps.push(my_creep);
// creeps.push(my_creep3);
// creeps.push(my_creep2);
<commit_msg>more creeps created<commit_after>//<!--  Contains functions to create, draw and modify creeps -->

var creeps = [];

function creep(posX,posY,direction,hitPoints){
	this.posX = posX;
	this.posY = posY;
	this.direction = direction;
	this.hitPoints = hitPoints;
	this.size = 10;
	this.image = new Image();
	this.image.src = imgIndifferentSmily;
	this.randomShort = 5;
	this.randomMedium = 5;
	this.randomLong = 5;
	this.deadCycle = undefined;
	this.drawSize = 50;

	this.drawCreep = drawCreep;
	function drawCreep(ctx){
		ctx.save();
	  ctx.translate(this.posX, this.posY);
	  ctx.rotate(to_rad(this.direction));
		ctx.drawImage(this.image,(this.drawSize/-2),(this.drawSize/-2), this.drawSize, this.drawSize);
	  ctx.restore();
	};
};

function drawAllCreeps(ctx){
	for (var i = 0; i < creeps.length; i++) {
    if (creeps[i] != undefined) {
      creeps[i].drawCreep(ctx);
    };
  };
};

function updateCreeps(){

	if (countCollection(creeps) < 11){
		createCreep(400,400);
	};

	console.log(countCollection(creeps));

	for (var i = 0; i < creeps.length; i++) {
    if (creeps[i] != undefined) {

	    creeps[i].direction = creeps[i].direction + ((creeps[i].randomMedium - 5) * .1);
	    creeps[i].posX = creeps[i].posX + ((creeps[i].randomShort - 5) * .5);
	    creeps[i].posY = creeps[i].posY + ((creeps[i].randomMedium - 5) * .3);

	    if (Math.abs(creeps[i].posX - 500) > 1000 || Math.abs(creeps[i].posY - 400)  > 800) {
	    	creeps[i].hitPoints = 0;
	    };

	    if (creeps[i].hitPoints < 1) {
	    	if (creeps[i].deadCycle == undefined) {
	    		creeps[i].deadCycle = gameLoopCounter;
	    	};
	    	deathRattle(creeps[i].posX, creeps[i].posY, i);
	    	if (creeps[i].deadCycle < (gameLoopCounter - 0)) {
	    		creeps[i] = undefined;
	    	};
	    };

	  };
	};
};

function hitCreep(i){
	creeps[towers[i].targetCreep].image.src = imgUnicorn;
	creeps[towers[i].targetCreep].drawSize = 100;
	creeps[towers[i].targetCreep].hitPoints = creeps[towers[i].targetCreep].hitPoints - 10;
};

function createCreep(x,y){
	if (gameLoopCounter % 60 == 1){
		console.log("create Creep!")
		creeps.push(new creep(x,y,0,20));
	}
};

function amIHit(creep,posX,posY,size){
	return false; // temporary!
};

function deathRattle(posX, posY, creepIndex){
	// alert("deathRattle - a creep has died")
	// console.log("deathRattle called")
};

function updateRandomFactors(collection){

		if (gameLoopCounter%30 == 1){	// SHORT random factor generator
		console.log("%30")
		for (var i = 0; i < creeps.length; i++) {
		    creeps[i].randomShort = to_i(((Math.random() * 10) + 1));
		    console.log(creeps[i].randomLong)
		};
	};

	if (gameLoopCounter%100 == 1){	// MEDIUM random factor generator
		console.log("%100")
		for (var i = 0; i < creeps.length; i++) {
		    creeps[i].randomMedium = to_i(((Math.random() * 10) + 1));
		    console.log(creeps[i].randomMedium)
		};
	};

	if (gameLoopCounter%1000 == 1){	// LONG random factor generator
		console.log("%1000")
		for (var i = 0; i < creeps.length; i++) {
		    creeps[i].randomLong = to_i(((Math.random() * 10) + 1));
		    console.log(creeps[i].randomLong)
		};
	};
}

// DRIVER CODE FOR DEV

// var my_creep = new creep(220,220,0,10);
// var my_creep2 = new creep(300,500,0,10);
// var my_creep3 = new creep(400,300,0,10);
// creeps.push(my_creep);
// creeps.push(my_creep3);
// creeps.push(my_creep2);

// var my_creep = new creep(200,200,0,10);
// var my_creep2 = new creep(400,500,0,10);
// var my_creep3 = new creep(200,500,0,10);
// creeps.push(my_creep);
// creeps.push(my_creep3);
// creeps.push(my_creep2);
<|endoftext|>


<commit_before>var express = require('express');
var router = express.Router();
var models = require("../../models");
var auth = require('../../authorization');
var log = require('../utils/logger');
var toJson = require('../utils/to_json');
var _ = require('lodash');

var getCommonWhereDateOptions = require('../engine/news_feeds/news_feeds_utils').getCommonWhereDateOptions;

var getNotifications = function (req, res, options, callback) {
  options = _.merge(options, {
    dateColumn: 'created_at'
  });

  if (req.query.afterDate) {
    options = _.merge(options, {
      afterDate: new Date(req.query.afterDate)
    })
  }

  if (req.query.beforeDate) {
    options = _.merge(options, {
      beforeDate: new Date(req.query.beforeDate)
    })
  }

  var where = _.merge({
    user_id: req.user.id
  }, getCommonWhereDateOptions(options));

  models.AcNotification.findAll({
    where: where,
    order: [
      ["created_at", "desc"]
    ],
    include: [
      {
        model: models.AcActivity,
        as: 'AcActivities',
        attributes: ['id','type','domain_id'],
        required: true,
        include: [
          {
            model: models.Post,
            required: false,
            attributes: ['id','name','user_id']
          },
          {
            model: models.User,
            required: false,
            attributes: ['id','name']
          },
          {
            model: models.Community,
            required: false,
            attributes: ['id','name']
          },
          {
            model: models.Group,
            required: false,
            attributes: ['id','name']
          },
          {
            model: models.Point,
            required: false,
            attributes: ['id']
          }
        ]
      }
    ],
    limit: 10
  }).then(function(notifications) {
    var slicedActivitesBecauseOfLimitBug = _.take(notifications, 7);
    res.send({
      notifications: slicedActivitesBecauseOfLimitBug,
      oldestProcessedNotificationAt: slicedActivitesBecauseOfLimitBug.length>0 ? _.last(slicedActivitesBecauseOfLimitBug).created_at : null
    });
    callback();
  }).catch(function(error) {
    callback(error);
  });
};

router.get('/', auth.isLoggedIn, function(req, res) {
  var options = {};
  getNotifications(req, res, options, function (error) {
    if (error) {
      log.error("Notifications Error", { userId: req.user ? req.user.id : null, errorStatus:  500 });
      res.sendStatus(500);
    }
  });
});

module.exports = router;<commit_msg>Work on notifications<commit_after>var express = require('express');
var router = express.Router();
var models = require("../../models");
var auth = require('../../authorization');
var log = require('../utils/logger');
var toJson = require('../utils/to_json');
var _ = require('lodash');

var getCommonWhereDateOptions = require('../engine/news_feeds/news_feeds_utils').getCommonWhereDateOptions;

var getNotifications = function (req, res, options, callback) {
  options = _.merge(options, {
    dateColumn: 'created_at'
  });

  if (req.query.afterDate) {
    options = _.merge(options, {
      afterDate: new Date(req.query.afterDate)
    })
  }

  if (req.query.beforeDate) {
    options = _.merge(options, {
      beforeDate: new Date(req.query.beforeDate)
    })
  }

  var where = _.merge({
    user_id: req.user.id
  }, getCommonWhereDateOptions(options));

  models.AcNotification.findAll({
    where: where,
    order: [
      ["created_at", "desc"]
    ],
    include: [
      {
        model: models.AcActivity,
        as: 'AcActivities',
        attributes: ['id','type','domain_id'],
        required: true,
        include: [
          {
            model: models.Post,
            required: false,
            attributes: ['id','name','user_id']
          },
          {
            model: models.User,
            required: false,
            attributes: ['id','name']
          },
          {
            model: models.Community,
            required: false,
            attributes: ['id','name']
          },
          {
            model: models.Group,
            required: false,
            attributes: ['id','name']
          },
          {
            model: models.Point,
            required: false,
            attributes: ['id','value']
          }
        ]
      }
    ],
    limit: 20
  }).then(function(notifications) {
    var slicedActivitesBecauseOfLimitBug = _.take(notifications, 20);
    res.send({
      notifications: slicedActivitesBecauseOfLimitBug,
      oldestProcessedNotificationAt: slicedActivitesBecauseOfLimitBug.length>0 ? _.last(slicedActivitesBecauseOfLimitBug).created_at : null
    });
    callback();
  }).catch(function(error) {
    callback(error);
  });
};

router.get('/', auth.isLoggedIn, function(req, res) {
  var options = {};
  getNotifications(req, res, options, function (error) {
    if (error) {
      log.error("Notifications Error", { userId: req.user ? req.user.id : null, errorStatus:  500 });
      res.sendStatus(500);
    }
  });
});

module.exports = router;<|endoftext|>


<commit_before>/* @flow */
import React from 'react';

import { Image, View } from 'react-native';
import { Text } from 'native-base';
import Moment from 'moment';
import styles from './styles';

type Props = {
  title: string,
  authors: string,
  thumbnailUrl: string,
  savedAt: Moment,
};

const PosterCard = (props: Props) =>
  <View style={styles.PosterCard}>
    <Image
      source={{ uri: props.thumbnailUrl }}
      style={styles.PosterThumbnail}
    />
    <View style={styles.PosterInfos}>
      <Text style={styles.PosterTitle}>
        {props.title}
      </Text>
      <Text style={styles.PosterAuthors}>
        {props.authors}
      </Text>
      <Text style={styles.PosterSavedAt}>
        Saved: {props.savedAt.format('YYYY/MM/DD, HH:mm a')}
      </Text>
    </View>
  </View>;

export default PosterCard;
<commit_msg>Make PosterCard touchable<commit_after>/* @flow */
import React from 'react';

import { Image, TouchableOpacity, View } from 'react-native';
import { Text } from 'native-base';

import styles from './styles';
import type { Poster } from '../types';

const PosterCard = (props: Poster) =>
  <TouchableOpacity>
    <View style={styles.PosterCard}>
      <Image
        source={{ uri: props.thumbnailUrl }}
        style={styles.PosterThumbnail}
      />
      <View style={styles.PosterInfos}>
        <Text style={styles.PosterTitle}>
          {props.title}
        </Text>
        <Text style={styles.PosterAuthors}>
          {props.authors}
        </Text>
        <Text style={styles.PosterSavedAt}>
          Saved: {props.savedAt.format('YYYY/MM/DD, HH:mm a')}
        </Text>
      </View>
    </View>
  </TouchableOpacity>;

export default PosterCard;
<|endoftext|>


<commit_before>import * as _ from 'lodash'

import hasDepartment from 'app/helpers/hasDepartment'
import {partialNameOrTitle} from 'app/helpers/partialTitle'
import {coursesAtLevel, coursesAtOrAboveLevel} from 'app/helpers/courseLevels'
import {checkCoursesFor} from 'app/helpers/courses'

import isRequiredCourse from 'sto-areas/lib/isRequiredCourse'

const ASIAN_REQUIRED_COURSES = [
	{deptnum: 'ASIAN 275'},

	{deptnum: 'ASIAN 397'}, {deptnum: 'ASIAN 399'},
]

let isRequiredAsianStudiesCourse = isRequiredCourse(ASIAN_REQUIRED_COURSES)

function interdisciplinaryApproachesToAsia(courses) {
	// Asian Studies 275: Interdisciplinary Approaches to Asia (.25 credit)
	return {
		title: 'Interdisciplinary Approaches to Asia',
		type: 'boolean',
		description: 'Asian Studies 275: Interdisciplinary Approaches to Asia',
		result: checkCoursesFor(courses, {deptnum:'ASIAN 275'}),
	}
}

function lowerLevelLanguageCourses(course) {
	// If all of these match, it is a lower-level language course, and will be
	// rejected by the `reject` method.
	return _.all([
		hasDepartment(['CHINA', 'JAPAN'], course),
		(course.level < 300),
	])
}

function electives(courses) {
	// Six electives, with stipulations:
	// 1. At least two at level II or level III, taken on campus;
	// 2. No more than two at level I;
	// 3. No more than four elective courses about any one country;
	// 4. No level I or level II language courses may count.

	let asianStudiesElectives = _(courses)
		.filter(hasDepartment('ASIAN'))
		.reject(lowerLevelLanguageCourses)
		.reject(isRequiredAsianStudiesCourse)
		.value()

	let levelsTwoOrThree = _(asianStudiesElectives)
		.filter(coursesAtOrAboveLevel(200))
		.size() >= 2

	let onlyTwoAtLevelOne = _(asianStudiesElectives)
		.filter(coursesAtLevel(100))
		.size() <= 2

	let notTooSpecialized = _.any([
		_(asianStudiesElectives).filter(partialNameOrTitle('China')).size() <= 4,
		_(asianStudiesElectives).filter(partialNameOrTitle('Japan')).size() <= 4,
	])

	let electivesAreGood = _.all(levelsTwoOrThree, onlyTwoAtLevelOne, notTooSpecialized)
	// console.log('asianStudiesElectives', asianStudiesElectives)

	let matching = _.size(asianStudiesElectives)
	let needs = 6

	// Req. #4 was embedded at the beginning, when we reject any lower-level
	// languages. That way, we can't count them.
	let details = [
		levelsTwoOrThree,
		onlyTwoAtLevelOne,
		notTooSpecialized,
	]
	return {
		title: 'Electives',
		type: 'object/number',
		description: 'Six electives, with stipulations: 1. At least two at level II or level III, taken on campus; 2. No more than two at level I; 3. No more than four elective courses about any one country; 4. No level I or level II language courses may count.',
		result: (matching >= needs) && electivesAreGood,
		details: {
			has: matching,
			needs: needs,
			matches: asianStudiesElectives,
		},
	}
}

function seniorSeminar(courses) {
	// Senior Seminar: One of:
	// - Asian Studies 397: Human Rights/Asian Context, or
	// - Asian Studies 399: Asian Studies Seminar
	let humanRights = checkCoursesFor(courses, {deptnum:'ASIAN 397'})
	let asiaSeminar = checkCoursesFor(courses, {deptnum:'ASIAN 399'})

	let seminars = [
		humanRights,
		asiaSeminar,
	]

	let seminarCount = _(seminars).compact().size()
	let seminarsNeeded = 1

	return {
		title: 'Senior Seminar',
		type: 'boolean',
		description: 'Senior Seminar: One of Asian Studies 397: Human Rights/Asian Context, or Asian Studies 399: Asian Studies Seminar',
		result: seminarCount >= seminarsNeeded,
	}
}

function language(courses) {
	// Two courses in Chinese or Japanese above 112 or its equivalent
	// "'JAPAN' IN depts AND level >= 200 AND (('Intermediate' OR 'Advanced') AND 'Japanese') IN title"
	// "'CHIN' IN depts AND level >= 200 AND (('Intermediate' OR 'Advanced') AND 'Chinese') IN title"
	let subsetOfCourses = _(courses)
		.filter(coursesAtOrAboveLevel(200))
		.filter((course) => partialNameOrTitle(['Intermediate', 'Advanced'], course))
		.value()

	let japaneseLanguage = _(subsetOfCourses)
		.filter(hasDepartment('JAPAN'))
		.filter(partialNameOrTitle('Japanese'))
		.value()

	let chineseLanguage = _(subsetOfCourses)
		.filter(hasDepartment('CHIN'))
		.filter(partialNameOrTitle('Chinese'))
		.value()

	let fulfilledLanguages = _.filter([japaneseLanguage, chineseLanguage], (courses) => _.size(courses) >= 2)
	let fulfilledLanguageCourses = _.flatten(fulfilledLanguages)

	let numberFulfilled = _.size(fulfilledLanguages)
	let numberNeeded = 1

	return {
		title: 'Language',
		type: 'object/number',
		description: 'Two courses in Chinese or Japanese above 112 or its equivalent',
		result: numberFulfilled >= numberNeeded,
		details: {
			has: numberFulfilled,
			needs: numberNeeded,
			matches: fulfilledLanguageCourses,
		},
	}
}

function checkAsianStudiesMajor(student) {
	return student.data().then((studentPieces) => {
		let {courses} = studentPieces

		let asianStudiesMajorRequirements = [
			interdisciplinaryApproachesToAsia(courses),
			electives(courses),
			language(courses),
			seniorSeminar(courses),
		]

		return {
			result: _.all(asianStudiesMajorRequirements, 'result'),
			details: asianStudiesMajorRequirements,
		}
	})
}

export default checkAsianStudiesMajor
<commit_msg>Bugfix: pass multiple things to check to _.all inside of an array<commit_after>import * as _ from 'lodash'

import hasDepartment from 'app/helpers/hasDepartment'
import {partialNameOrTitle} from 'app/helpers/partialTitle'
import {coursesAtLevel, coursesAtOrAboveLevel} from 'app/helpers/courseLevels'
import {checkCoursesFor} from 'app/helpers/courses'

import isRequiredCourse from 'sto-areas/lib/isRequiredCourse'

const ASIAN_REQUIRED_COURSES = [
	{deptnum: 'ASIAN 275'},

	{deptnum: 'ASIAN 397'}, {deptnum: 'ASIAN 399'},
]

let isRequiredAsianStudiesCourse = isRequiredCourse(ASIAN_REQUIRED_COURSES)

function interdisciplinaryApproachesToAsia(courses) {
	// Asian Studies 275: Interdisciplinary Approaches to Asia (.25 credit)
	return {
		title: 'Interdisciplinary Approaches to Asia',
		type: 'boolean',
		description: 'Asian Studies 275: Interdisciplinary Approaches to Asia',
		result: checkCoursesFor(courses, {deptnum:'ASIAN 275'}),
	}
}

function lowerLevelLanguageCourses(course) {
	// If all of these match, it is a lower-level language course, and will be
	// rejected by the `reject` method.
	return _.all([
		hasDepartment(['CHINA', 'JAPAN'], course),
		(course.level < 300),
	])
}

function electives(courses) {
	// Six electives, with stipulations:
	// 1. At least two at level II or level III, taken on campus;
	// 2. No more than two at level I;
	// 3. No more than four elective courses about any one country;
	// 4. No level I or level II language courses may count.

	let asianStudiesElectives = _(courses)
		.filter(hasDepartment('ASIAN'))
		.reject(lowerLevelLanguageCourses)
		.reject(isRequiredAsianStudiesCourse)
		.value()

	let levelsTwoOrThree = _(asianStudiesElectives)
		.filter(coursesAtOrAboveLevel(200))
		.size() >= 2

	let onlyTwoAtLevelOne = _(asianStudiesElectives)
		.filter(coursesAtLevel(100))
		.size() <= 2

	let notTooSpecialized = _.any([
		_(asianStudiesElectives).filter(partialNameOrTitle('China')).size() <= 4,
		_(asianStudiesElectives).filter(partialNameOrTitle('Japan')).size() <= 4,
	])

	let electivesAreGood = _.all([levelsTwoOrThree, onlyTwoAtLevelOne, notTooSpecialized])
	// console.log('asianStudiesElectives', asianStudiesElectives)

	let matching = _.size(asianStudiesElectives)
	let needs = 6

	// Req. #4 was embedded at the beginning, when we reject any lower-level
	// languages. That way, we can't count them.
	let details = [
		levelsTwoOrThree,
		onlyTwoAtLevelOne,
		notTooSpecialized,
	]
	return {
		title: 'Electives',
		type: 'object/number',
		description: 'Six electives, with stipulations: 1. At least two at level II or level III, taken on campus; 2. No more than two at level I; 3. No more than four elective courses about any one country; 4. No level I or level II language courses may count.',
		result: (matching >= needs) && electivesAreGood,
		details: {
			has: matching,
			needs: needs,
			matches: asianStudiesElectives,
		},
	}
}

function seniorSeminar(courses) {
	// Senior Seminar: One of:
	// - Asian Studies 397: Human Rights/Asian Context, or
	// - Asian Studies 399: Asian Studies Seminar
	let humanRights = checkCoursesFor(courses, {deptnum:'ASIAN 397'})
	let asiaSeminar = checkCoursesFor(courses, {deptnum:'ASIAN 399'})

	let seminars = [
		humanRights,
		asiaSeminar,
	]

	let seminarCount = _(seminars).compact().size()
	let seminarsNeeded = 1

	return {
		title: 'Senior Seminar',
		type: 'boolean',
		description: 'Senior Seminar: One of Asian Studies 397: Human Rights/Asian Context, or Asian Studies 399: Asian Studies Seminar',
		result: seminarCount >= seminarsNeeded,
	}
}

function language(courses) {
	// Two courses in Chinese or Japanese above 112 or its equivalent
	// "'JAPAN' IN depts AND level >= 200 AND (('Intermediate' OR 'Advanced') AND 'Japanese') IN title"
	// "'CHIN' IN depts AND level >= 200 AND (('Intermediate' OR 'Advanced') AND 'Chinese') IN title"
	let subsetOfCourses = _(courses)
		.filter(coursesAtOrAboveLevel(200))
		.filter((course) => partialNameOrTitle(['Intermediate', 'Advanced'], course))
		.value()

	let japaneseLanguage = _(subsetOfCourses)
		.filter(hasDepartment('JAPAN'))
		.filter(partialNameOrTitle('Japanese'))
		.value()

	let chineseLanguage = _(subsetOfCourses)
		.filter(hasDepartment('CHIN'))
		.filter(partialNameOrTitle('Chinese'))
		.value()

	let fulfilledLanguages = _.filter([japaneseLanguage, chineseLanguage], (courses) => _.size(courses) >= 2)
	let fulfilledLanguageCourses = _.flatten(fulfilledLanguages)

	let numberFulfilled = _.size(fulfilledLanguages)
	let numberNeeded = 1

	return {
		title: 'Language',
		type: 'object/number',
		description: 'Two courses in Chinese or Japanese above 112 or its equivalent',
		result: numberFulfilled >= numberNeeded,
		details: {
			has: numberFulfilled,
			needs: numberNeeded,
			matches: fulfilledLanguageCourses,
		},
	}
}

function checkAsianStudiesMajor(student) {
	return student.data().then((studentPieces) => {
		let {courses} = studentPieces

		let asianStudiesMajorRequirements = [
			interdisciplinaryApproachesToAsia(courses),
			electives(courses),
			language(courses),
			seniorSeminar(courses),
		]

		return {
			result: _.all(asianStudiesMajorRequirements, 'result'),
			details: asianStudiesMajorRequirements,
		}
	})
}

export default checkAsianStudiesMajor
<|endoftext|>


<commit_before>// Base BusinessProcess model

'use strict';

var memoize                   = require('memoizee/plain')
  , defineStringLine          = require('dbjs-ext/string/string-line')
  , defineUInteger            = require('dbjs-ext/number/integer/u-integer')
  , defineUrl                 = require('dbjs-ext/string/string-line/url')
  , _                         = require('mano').i18n.bind("Model: Business Process")
  , defineBusinessProcessBase = require('../lib/business-process-base')
  , defineNestedMap           = require('../lib/nested-map')
  , defineStatusLog           = require('../lib/status-log')
  , defineStatusHistoryItem   = require('../lib/status-history-item');

module.exports = memoize(function (db/*, options*/) {
	var options             = Object(arguments[1])
	  , BusinessProcessBase = defineBusinessProcessBase(db)
	  , StringLine          = defineStringLine(db)
	  , UInteger            = defineUInteger(db)
	  , Url                 = defineUrl(db)
	  , StatusLog           = defineStatusLog(db)
	  , StatusHistoryItem   = defineStatusHistoryItem(db)

	  , BusinessProcess;

	defineNestedMap(db);

	BusinessProcess = BusinessProcessBase.extend(options.className || 'BusinessProcess', {
		// General label of business process type
		// It's not specific per business process, but should provide general info as:
		// "Merchant registration", "Company registration" etc.
		label: { type: StringLine },

		// Should contain some action i.e. "start a company"
		registerServiceLabel: { type: StringLine },

		serviceDescription: { type: db.String },

		// General abbreviation of business process type
		// e.g. IT (Individual trader), C (Company), COI (Certificate of incentives)
		abbr: { type: StringLine },

		// Name of businessProcess
		// Usually computed from other properties
		businessName: { type: StringLine, label: _("Business name")  },

		// URL at which archive of files related to given business process
		// is accessible
		filesArchiveUrl: { type: Url },

		submissionNumber: { type: db.Object, nested: true },

		// Whether registration is made online
		// It may be overriden to false in case we import businessProces
		// which not processed electronically.
		// It has its use in "business update" applications where we allow updates
		// of old registrations done at the counter
		isFromEregistrations: { type: db.Boolean, value: true,
			label: _("Has registration been made online?") },

		// Whether business process was created for demo purposes
		isDemo: { type: db.Boolean, value: false },

		// String over which business processes can be searched
		// through interface panel (computed value is later indexed by persistence engine)
		searchString: { type: db.String, value: function (_observe) {
			var arr = [], submissionNumber = _observe(this.submissionNumber._value);
			if (this.businessName) arr.push(this.businessName.toLowerCase());
			if (submissionNumber) arr.push(submissionNumber.toLowerCase());
			return arr.join('\x02');
		} },

		// An array of email addresses used as notification recipients (to field).
		// Uses user and manager emails, if the exist, by default.
		notificationEmails: { type: StringLine, multiple: true, value: function (_observe) {
			var result = [];

			if (this.user) result.push(_observe(this.user._email));
			if (this.manager) result.push(_observe(this.manager._email));

			return result;
		} },
		toWebServiceJSON: {
			value: function (ignore) {
				var dataFields = {}, result, db;
				db = this.database;
				result = {
					id: this.__id__,
					service: {
						code: this.constructor.__id__
					},
					submittedTimestamp:
						this._isSubmitted.lastModified ?
								Math.floor(this._isSubmitted.lastModified / 1000) : null,
					processingSteps: {},
					request: {
						registrations: [],
						documentUploads: [],
						costs: [],
						payments: [],
						certificates: [],
						data: {}
					}
				};

				this.processingSteps.map.forEach(function self(processingStep) {
					if (db.ProcessingStepGroup && processingStep instanceof db.ProcessingStepGroup) {
						processingStep.steps.applicable.forEach(self);
						return;
					}
					result.processingSteps[processingStep.key] = processingStep.toWebServiceJSON();
				});

				this.registrations.requested.forEach(function (reg) {
					result.request.registrations.push({
						code: reg.key
					});
				});

				this.requirementUploads.applicable.forEach(function (upload) {
					result.request.documentUploads.push(upload.toWebServiceJSON());
				});

				this.costs.payable.forEach(function (cost) {
					result.request.costs.push(cost.toWebServiceJSON());
				});

				this.paymentReceiptUploads.applicable.forEach(function (payment) {
					result.request.payments.push(payment.toWebServiceJSON());
				});

				this.certificates.applicable.forEach(function (certificate) {
					result.request.certificates.push(certificate.toWebServiceJSON());
				});

				// toWebServiceJSON is not implemented on FormSectionBase
				if (this.database.FormSectionBase &&
						this.determinants.constructor !== this.database.FormSectionBase) {
					dataFields = this.determinants.toWebServiceJSON();
					Object.keys(dataFields).forEach(function (fieldName) {
						result.request.data[fieldName] = dataFields[fieldName];
					});
				}
				this.dataForms.applicable.forEach(function (section) {
					dataFields = section.toWebServiceJSON();
					Object.keys(dataFields).forEach(function (fieldName) {
						result.request.data[fieldName] = dataFields[fieldName];
					});
				});
				this.submissionForms.applicable.forEach(function (section) {
					dataFields = section.toWebServiceJSON();
					Object.keys(dataFields).forEach(function (fieldName) {
						result.request.data[fieldName] = dataFields[fieldName];
					});
				});

				return result;
			}
		},
		toMetaDataJSON: {
			type: db.Function,
			value: function (opts) {
				var fields = [];
				this.dataForms.map.forEach(function (section) {
					try {
						fields = fields.concat(section.toMetaDataJSON());
					} catch (ignore) {}
				});

				return fields;
			}
		},
		toWSSchema: {
			value: function (ignore) {
				var formSchema
				  , db = this.database
				  , schema = {
					typeName: this.constructor.__id__,
					properties: {
						id: { type: "string" },
						service: { type: "enum", ref: "services" },
						submittedTimestamp: { type: "timestamp" },
						processingSteps: { type: "object", properties: {} },
						request: {
							type: "object",
							properties: {
								registrations: {},
								documentUploads: {},
								costs: {},
								payments: {},
								certificates: {},
								data: {
									dataForms: [],
									type: "object",
									properties: {}
								}
							}
						}
					}
				};

				var assignForm = function (form) {
					formSchema = form.toWSSchema();
					if (formSchema.dataForms) {
						//dataForms will have to be set via iteration because assign
						//will overwrite existing value of schema dataForms property.
						formSchema.dataForms.forEach(function (dataForm) {
							schema.properties.request.properties.data.dataForms.push(dataForm);
						});
						delete formSchema.dataForms;
					}

					Object.assign(schema.properties.request.properties.data.properties, formSchema);
				};

				this.constructor.prototype.processingSteps.map.forEach(function self(processingStep) {
					if (db.ProcessingStepGroup && processingStep instanceof db.ProcessingStepGroup) {
						processingStep.steps.map.forEach(self);
						return;
					}
					schema.properties.processingSteps.properties[processingStep.key] =
						processingStep.toWSSchema();
				});

				schema.properties.request.properties.registrations =
					this.registrations.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.costs =
					this.costs.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.certificates =
					this.certificates.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.documentUploads =
					this.requirementUploads.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.payments =
					this.paymentReceiptUploads.map._descriptorPrototype_.type.prototype.toWSSchema();

				// guide
				// toWSSchema is not implemented on FormSectionBase
				if (db.FormSectionBase &&
						this.determinants.constructor !== db.FormSectionBase) {
					assignForm(this.constructor.prototype.determinants);
				}

				// dataForms
				this.constructor.prototype.dataForms.map.forEach(function (form) {
					if (db.FormSectionBase &&
							form.constructor !== db.FormSectionBase) {
						assignForm(form);
					}
				});

				//submissionForms
				this.constructor.prototype.submissionForms.map.forEach(function (form) {
					if (db.FormSectionBase &&
							form.constructor !== db.FormSectionBase) {
						assignForm(form);
					}
				});

				return schema;
			}
		}
	}, {
		draftLimit: { type: UInteger, value: 20 }
	});

	BusinessProcess.prototype.submissionNumber.defineProperties({
		// Stringified complete representation of number
		value: { type: StringLine, value: function () { return this.number; } },
		// Numeric part of number (usually incremented for each file)
		number: { type: UInteger, value: 0 },
		// Convinient stringification
		toString: { value: function (opts) { return this.value; } }
	});

	BusinessProcess.prototype.defineNestedMap('statusLog',
		{ itemType: StatusLog, cardinalPropertyKey: 'label' });

	BusinessProcess.prototype.defineNestedMap('statusHistory',
		{ itemType: StatusHistoryItem, cardinalPropertyKey: 'status' });

	return BusinessProcess;
}, { normalizer: require('memoizee/normalizers/get-1')() });
<commit_msg>special handling for FormSectionGroup<commit_after>// Base BusinessProcess model

'use strict';

var memoize                   = require('memoizee/plain')
  , defineStringLine          = require('dbjs-ext/string/string-line')
  , defineUInteger            = require('dbjs-ext/number/integer/u-integer')
  , defineUrl                 = require('dbjs-ext/string/string-line/url')
  , _                         = require('mano').i18n.bind("Model: Business Process")
  , defineBusinessProcessBase = require('../lib/business-process-base')
  , defineNestedMap           = require('../lib/nested-map')
  , defineStatusLog           = require('../lib/status-log')
  , defineStatusHistoryItem   = require('../lib/status-history-item');

module.exports = memoize(function (db/*, options*/) {
	var options             = Object(arguments[1])
	  , BusinessProcessBase = defineBusinessProcessBase(db)
	  , StringLine          = defineStringLine(db)
	  , UInteger            = defineUInteger(db)
	  , Url                 = defineUrl(db)
	  , StatusLog           = defineStatusLog(db)
	  , StatusHistoryItem   = defineStatusHistoryItem(db)

	  , BusinessProcess;

	defineNestedMap(db);

	BusinessProcess = BusinessProcessBase.extend(options.className || 'BusinessProcess', {
		// General label of business process type
		// It's not specific per business process, but should provide general info as:
		// "Merchant registration", "Company registration" etc.
		label: { type: StringLine },

		// Should contain some action i.e. "start a company"
		registerServiceLabel: { type: StringLine },

		serviceDescription: { type: db.String },

		// General abbreviation of business process type
		// e.g. IT (Individual trader), C (Company), COI (Certificate of incentives)
		abbr: { type: StringLine },

		// Name of businessProcess
		// Usually computed from other properties
		businessName: { type: StringLine, label: _("Business name")  },

		// URL at which archive of files related to given business process
		// is accessible
		filesArchiveUrl: { type: Url },

		submissionNumber: { type: db.Object, nested: true },

		// Whether registration is made online
		// It may be overriden to false in case we import businessProces
		// which not processed electronically.
		// It has its use in "business update" applications where we allow updates
		// of old registrations done at the counter
		isFromEregistrations: { type: db.Boolean, value: true,
			label: _("Has registration been made online?") },

		// Whether business process was created for demo purposes
		isDemo: { type: db.Boolean, value: false },

		// String over which business processes can be searched
		// through interface panel (computed value is later indexed by persistence engine)
		searchString: { type: db.String, value: function (_observe) {
			var arr = [], submissionNumber = _observe(this.submissionNumber._value);
			if (this.businessName) arr.push(this.businessName.toLowerCase());
			if (submissionNumber) arr.push(submissionNumber.toLowerCase());
			return arr.join('\x02');
		} },

		// An array of email addresses used as notification recipients (to field).
		// Uses user and manager emails, if the exist, by default.
		notificationEmails: { type: StringLine, multiple: true, value: function (_observe) {
			var result = [];

			if (this.user) result.push(_observe(this.user._email));
			if (this.manager) result.push(_observe(this.manager._email));

			return result;
		} },
		toWebServiceJSON: {
			value: function (ignore) {
				var dataFields = {}, result, db;
				db = this.database;
				result = {
					id: this.__id__,
					service: {
						code: this.constructor.__id__
					},
					submittedTimestamp:
						this._isSubmitted.lastModified ?
								Math.floor(this._isSubmitted.lastModified / 1000) : null,
					processingSteps: {},
					request: {
						registrations: [],
						documentUploads: [],
						costs: [],
						payments: [],
						certificates: [],
						data: {}
					}
				};

				this.processingSteps.map.forEach(function self(processingStep) {
					if (db.ProcessingStepGroup && processingStep instanceof db.ProcessingStepGroup) {
						processingStep.steps.applicable.forEach(self);
						return;
					}
					result.processingSteps[processingStep.key] = processingStep.toWebServiceJSON();
				});

				this.registrations.requested.forEach(function (reg) {
					result.request.registrations.push({
						code: reg.key
					});
				});

				this.requirementUploads.applicable.forEach(function (upload) {
					result.request.documentUploads.push(upload.toWebServiceJSON());
				});

				this.costs.payable.forEach(function (cost) {
					result.request.costs.push(cost.toWebServiceJSON());
				});

				this.paymentReceiptUploads.applicable.forEach(function (payment) {
					result.request.payments.push(payment.toWebServiceJSON());
				});

				this.certificates.applicable.forEach(function (certificate) {
					result.request.certificates.push(certificate.toWebServiceJSON());
				});

				// toWebServiceJSON is not implemented on FormSectionBase
				if (this.database.FormSectionBase &&
						this.determinants.constructor !== this.database.FormSectionBase) {
					dataFields = this.determinants.toWebServiceJSON();
					Object.keys(dataFields).forEach(function (fieldName) {
						result.request.data[fieldName] = dataFields[fieldName];
					});
				}
				this.dataForms.applicable.forEach(function (section) {
					dataFields = section.toWebServiceJSON();
					Object.keys(dataFields).forEach(function (fieldName) {
						result.request.data[fieldName] = dataFields[fieldName];
					});
				});
				this.submissionForms.applicable.forEach(function (section) {
					dataFields = section.toWebServiceJSON();
					Object.keys(dataFields).forEach(function (fieldName) {
						result.request.data[fieldName] = dataFields[fieldName];
					});
				});

				return result;
			}
		},
		toMetaDataJSON: {
			type: db.Function,
			value: function (opts) {
				var fields = [];
				this.dataForms.map.forEach(function (section) {
					try {
						fields = fields.concat(section.toMetaDataJSON());
					} catch (ignore) {}
				});

				return fields;
			}
		},
		toWSSchema: {
			value: function (ignore) {
				var formSchema
				  , db = this.database
				  , schema = {
					typeName: this.constructor.__id__,
					properties: {
						id: { type: "string" },
						service: { type: "enum", ref: "services" },
						submittedTimestamp: { type: "timestamp" },
						processingSteps: { type: "object", properties: {} },
						request: {
							type: "object",
							properties: {
								registrations: {},
								documentUploads: {},
								costs: {},
								payments: {},
								certificates: {},
								data: {
									dataForms: [],
									type: "object",
									properties: {}
								}
							}
						}
					}
				};

				var assignForm = function (form) {
					formSchema = form.toWSSchema();
					if (formSchema.dataForms) {
						//handling of db.FormSection type dataForm
						//dataForms will have to be set via iteration because assign
						//will overwrite existing value of schema dataForms property.
						formSchema.dataForms.forEach(function (dataForm) {
							schema.properties.request.properties.data.dataForms.push(dataForm);
						});
						delete formSchema.dataForms;
					}
					else if (Object.keys(formSchema).length == 1
							&& formSchema[Object.keys(formSchema)[0]].dataForms){
						//handling of db.FormSectionGroup type dataForm.
						//dataForms will have to be set via iteration because assign
						//will overwrite existing value of schema dataForms property.
						formSchema[Object.keys(formSchema)[0]].dataForms.forEach(function (dataForm) {
							schema.properties.request.properties.data.dataForms.push(dataForm);
						});
						delete formSchema[Object.keys(formSchema)[0]].dataForms;
					}

					Object.assign(schema.properties.request.properties.data.properties, formSchema);
				};

				this.constructor.prototype.processingSteps.map.forEach(function self(processingStep) {
					if (db.ProcessingStepGroup && processingStep instanceof db.ProcessingStepGroup) {
						processingStep.steps.map.forEach(self);
						return;
					}
					schema.properties.processingSteps.properties[processingStep.key] =
						processingStep.toWSSchema();
				});

				schema.properties.request.properties.registrations =
					this.registrations.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.costs =
					this.costs.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.certificates =
					this.certificates.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.documentUploads =
					this.requirementUploads.map._descriptorPrototype_.type.prototype.toWSSchema();
				schema.properties.request.properties.payments =
					this.paymentReceiptUploads.map._descriptorPrototype_.type.prototype.toWSSchema();

				// guide
				// toWSSchema is not implemented on FormSectionBase
				if (db.FormSectionBase &&
						this.determinants.constructor !== db.FormSectionBase) {
					assignForm(this.constructor.prototype.determinants);
				}

				// dataForms
				this.constructor.prototype.dataForms.map.forEach(function (form) {
					if (db.FormSectionBase &&
							form.constructor !== db.FormSectionBase) {
						assignForm(form);
					}
				});

				//submissionForms
				this.constructor.prototype.submissionForms.map.forEach(function (form) {
					if (db.FormSectionBase &&
							form.constructor !== db.FormSectionBase) {
						assignForm(form);
					}
				});

				return schema;
			}
		}
	}, {
		draftLimit: { type: UInteger, value: 20 }
	});

	BusinessProcess.prototype.submissionNumber.defineProperties({
		// Stringified complete representation of number
		value: { type: StringLine, value: function () { return this.number; } },
		// Numeric part of number (usually incremented for each file)
		number: { type: UInteger, value: 0 },
		// Convinient stringification
		toString: { value: function (opts) { return this.value; } }
	});

	BusinessProcess.prototype.defineNestedMap('statusLog',
		{ itemType: StatusLog, cardinalPropertyKey: 'label' });

	BusinessProcess.prototype.defineNestedMap('statusHistory',
		{ itemType: StatusHistoryItem, cardinalPropertyKey: 'status' });

	return BusinessProcess;
}, { normalizer: require('memoizee/normalizers/get-1')() });
<|endoftext|>


<commit_before>// Flow properties of BusinessProcess model

'use strict';

var _                           = require('mano').i18n.bind('Model')
  , memoize                     = require('memoizee/plain')
  , Map                         = require('es6-map')
  , defineStringLine            = require('dbjs-ext/string/string-line')
  , defineBusinessProcessStatus = require('../lib/business-process-status')
  , defineBusinessProcess       = require('./guide')
  , defineDataForms             = require('./data-forms')
  , defineRequirementUploads    = require('./requirement-uploads')
  , defineCosts                 = require('./costs')
  , defineSubmissionForms       = require('./submission-forms')
  , defineProcessingSteps       = require('./processing-steps')
  , defineUInteger              = require('dbjs-ext/number/integer/u-integer')
  , definePerson                = require('../person');

module.exports = memoize(function (db/*, options*/) {
	var options               = Object(arguments[1])
	  , BusinessProcess       = defineBusinessProcess(db, options)
	  , BusinessProcessStatus = defineBusinessProcessStatus(db)
	  , UInteger              = defineUInteger(db)
	  , StringLine            = defineStringLine(db)
	  , Person                = definePerson(db, options);

	defineDataForms(db, options);
	defineRequirementUploads(db, options);
	defineCosts(db, options);
	defineSubmissionForms(db, options);
	defineProcessingSteps(db, options);

	// Enum for submitterType property
	var SubmitterType = StringLine.createEnum('SubmitterType', new Map([
		['user', { label: _("User") }],
		['manager', { label: _("Manager") }]
	]));

	BusinessProcess.prototype.defineProperties({

		// Whether file is complete at Part A and submitted to Part B
		isSubmittedReady: { type: db.Boolean, value: function (_observe) {
			// 0. Guide
			if (this.guideProgress !== 1) return false;
			// 1. Forms
			if (_observe(this.dataForms._progress) !== 1) return false;
			// 2. Uploads
			if (_observe(this.requirementUploads._progress) !== 1) return false;
			// 3. Payments
			if (_observe(this.costs._paymentProgress) !== 1) return false;
			// 4. Submission
			if (_observe(this.submissionForms._progress) !== 1) return false;
			return true;
		} },

		// Whether file was submitted to Part B
		// Set to true by server service on first successful request submission
		// (technically: whenever isSubmittedReady turns true for very first time)
		isSubmitted: { type: db.Boolean, value: false },

		// The User that submitted application to Part B
		// This will be the original submitter. If the file was sent back and resubmitted by
		// other user, this property will not change.
		submitter: { type: Person },

		// Set in application-submit controller for registration apps.
		// This will be the original submitter type. If the file was sent back and resubmitted by
		// user of different type, this property will not change.
		submitterType: { type: SubmitterType, value: 'user' },

		// Whether business process was sent back to Part A
		isSentBack: { type: db.Boolean, value: false },

		// Whether business process is ready for being processed by the User (in scope of Part B)
		// To be replaced with proper logic at end system
		//
		// As user processing resembles steps like 'sentBack' or 'redelegated',
		// this property (not like isSubmitted) should switch resolution value to *false*
		// when user processing is finalized.
		// Technically (with help of locker properties that are out of scope of base model)
		// it can be configured to handle more than one user processing step.
		isUserProcessingReady: { type: db.Boolean, value: false },

		// Whether business process is being process by User (in scope of Part B)
		// Set to true by server service whenever isUserProcessingReady turns true
		isUserProcessing: { type: db.Boolean, value: false },

		// Whether business process is at draft stage (Part A)
		isAtDraft: { type: db.Boolean, value: function () {
			return !this.isSubmitted || this.isSentBack || this.isUserProcessing;
		} },
		// Whether business is approved
		isApprovedReady: { type: db.Boolean, value: function (_observe) {
			if (!this.isSubmitted) return false;
			return _observe(this.processingSteps.applicable).every(function (step) {
				return _observe(step._isApproved);
			});
		} },
		// Whether business process was approved
		// Set to true by server service when isApprovedReady turns true
		isApproved: { type: db.Boolean, value: false },
		// Whether business process was rejected
		// Set to true by server service when first processing step is rejected
		isRejected: { type: db.Boolean, value: false },

		// Whether business process is closed
		isClosed: { type: db.Boolean, value: function (_observe) {
			return this.isRejected || this.isApproved;
		} },
		// Computed status of registration
		status: { type: BusinessProcessStatus, value: function (_observe) {
			var frontDesk;
			if (!this.isSubmitted) return 'draft';
			if (this.isSentBack) return 'sentBack';
			if (this.isRejected) return 'rejected';
			if (this.isClosed) return 'closed';

			if (_observe(this.processingSteps.applicable).some(function (step) {
					return _observe(step._status) === 'rejected';
				})) {
				return 'rejected';
			}
			if (this.processingSteps.applicable.some(function (step) {
					return _observe(step._status) === 'sentBack' && _observe(step._isPreviousStepsSatisfied);
				})) {
				return 'sentBack';
			}
			if (this.processingSteps.applicable.every(function (step) {
					return _observe(step._status) === 'approved';
				})) {
				return 'closed';
			}

			if (this.processingSteps.revisions.some(function (processingStep) {
					return _observe(processingStep._isRevisionPending) ||
						(_observe(processingStep._status) === 'approved' &&
							!_observe(processingStep._isSatisfied));
				})) {
				return 'revision';
			}

			frontDesk = this.processingSteps.frontDesk;
			if (frontDesk && _observe(frontDesk._isApproved)) return 'withdrawn';
			if (frontDesk && _observe(frontDesk._isPending)) return 'pickup';

			return 'process';
		} },
		correctionTime: { type: UInteger, value: 0 },
		processingHolidaysTime: { type: UInteger, value: 0 }
	});

	return BusinessProcess;
}, { normalizer: require('memoizee/normalizers/get-1')() });
<commit_msg>#1757 half revision half processing steps are now handled as revisions<commit_after>// Flow properties of BusinessProcess model

'use strict';

var _                           = require('mano').i18n.bind('Model')
  , memoize                     = require('memoizee/plain')
  , Map                         = require('es6-map')
  , defineStringLine            = require('dbjs-ext/string/string-line')
  , defineBusinessProcessStatus = require('../lib/business-process-status')
  , defineBusinessProcess       = require('./guide')
  , defineDataForms             = require('./data-forms')
  , defineRequirementUploads    = require('./requirement-uploads')
  , defineCosts                 = require('./costs')
  , defineSubmissionForms       = require('./submission-forms')
  , defineProcessingSteps       = require('./processing-steps')
  , defineUInteger              = require('dbjs-ext/number/integer/u-integer')
  , definePerson                = require('../person');

module.exports = memoize(function (db/*, options*/) {
	var options               = Object(arguments[1])
	  , BusinessProcess       = defineBusinessProcess(db, options)
	  , BusinessProcessStatus = defineBusinessProcessStatus(db)
	  , UInteger              = defineUInteger(db)
	  , StringLine            = defineStringLine(db)
	  , Person                = definePerson(db, options);

	defineDataForms(db, options);
	defineRequirementUploads(db, options);
	defineCosts(db, options);
	defineSubmissionForms(db, options);
	defineProcessingSteps(db, options);

	// Enum for submitterType property
	var SubmitterType = StringLine.createEnum('SubmitterType', new Map([
		['user', { label: _("User") }],
		['manager', { label: _("Manager") }]
	]));

	BusinessProcess.prototype.defineProperties({

		// Whether file is complete at Part A and submitted to Part B
		isSubmittedReady: { type: db.Boolean, value: function (_observe) {
			// 0. Guide
			if (this.guideProgress !== 1) return false;
			// 1. Forms
			if (_observe(this.dataForms._progress) !== 1) return false;
			// 2. Uploads
			if (_observe(this.requirementUploads._progress) !== 1) return false;
			// 3. Payments
			if (_observe(this.costs._paymentProgress) !== 1) return false;
			// 4. Submission
			if (_observe(this.submissionForms._progress) !== 1) return false;
			return true;
		} },

		// Whether file was submitted to Part B
		// Set to true by server service on first successful request submission
		// (technically: whenever isSubmittedReady turns true for very first time)
		isSubmitted: { type: db.Boolean, value: false },

		// The User that submitted application to Part B
		// This will be the original submitter. If the file was sent back and resubmitted by
		// other user, this property will not change.
		submitter: { type: Person },

		// Set in application-submit controller for registration apps.
		// This will be the original submitter type. If the file was sent back and resubmitted by
		// user of different type, this property will not change.
		submitterType: { type: SubmitterType, value: 'user' },

		// Whether business process was sent back to Part A
		isSentBack: { type: db.Boolean, value: false },

		// Whether business process is ready for being processed by the User (in scope of Part B)
		// To be replaced with proper logic at end system
		//
		// As user processing resembles steps like 'sentBack' or 'redelegated',
		// this property (not like isSubmitted) should switch resolution value to *false*
		// when user processing is finalized.
		// Technically (with help of locker properties that are out of scope of base model)
		// it can be configured to handle more than one user processing step.
		isUserProcessingReady: { type: db.Boolean, value: false },

		// Whether business process is being process by User (in scope of Part B)
		// Set to true by server service whenever isUserProcessingReady turns true
		isUserProcessing: { type: db.Boolean, value: false },

		// Whether business process is at draft stage (Part A)
		isAtDraft: { type: db.Boolean, value: function () {
			return !this.isSubmitted || this.isSentBack || this.isUserProcessing;
		} },
		// Whether business is approved
		isApprovedReady: { type: db.Boolean, value: function (_observe) {
			if (!this.isSubmitted) return false;
			return _observe(this.processingSteps.applicable).every(function (step) {
				return _observe(step._isApproved);
			});
		} },
		// Whether business process was approved
		// Set to true by server service when isApprovedReady turns true
		isApproved: { type: db.Boolean, value: false },
		// Whether business process was rejected
		// Set to true by server service when first processing step is rejected
		isRejected: { type: db.Boolean, value: false },

		// Whether business process is closed
		isClosed: { type: db.Boolean, value: function (_observe) {
			return this.isRejected || this.isApproved;
		} },
		// Computed status of registration
		status: { type: BusinessProcessStatus, value: function (_observe) {
			var frontDesk;
			if (!this.isSubmitted) return 'draft';
			if (this.isSentBack) return 'sentBack';
			if (this.isRejected) return 'rejected';
			if (this.isClosed) return 'closed';

			if (_observe(this.processingSteps.applicable).some(function (step) {
					return _observe(step._status) === 'rejected';
				})) {
				return 'rejected';
			}
			if (this.processingSteps.applicable.some(function (step) {
					return _observe(step._status) === 'sentBack' && _observe(step._isPreviousStepsSatisfied);
				})) {
				return 'sentBack';
			}
			if (this.processingSteps.applicable.every(function (step) {
					return _observe(step._status) === 'approved';
				})) {
				return 'closed';
			}

			if (this.processingSteps.revisions.some(function (processingStep) {
					return _observe(processingStep._isPending) ||
						(_observe(processingStep._status) === 'approved' &&
							!_observe(processingStep._isSatisfied));
				})) {
				return 'revision';
			}

			frontDesk = this.processingSteps.frontDesk;
			if (frontDesk && _observe(frontDesk._isApproved)) return 'withdrawn';
			if (frontDesk && _observe(frontDesk._isPending)) return 'pickup';

			return 'process';
		} },
		correctionTime: { type: UInteger, value: 0 },
		processingHolidaysTime: { type: UInteger, value: 0 }
	});

	return BusinessProcess;
}, { normalizer: require('memoizee/normalizers/get-1')() });
<|endoftext|>


<commit_before>/*global require*/
require(["gtfs-exchange"], function (Agencies) {
	"use strict";
	var agencyListUrl = "proxy.ashx?http://www.gtfs-data-exchange.com/api/agencies";

	function isInWA(val) {
		return val && val.country === "United States" && val.state === "Washington";
	}

	function createAgencyTable(/**Object[]*/ agencies) {
		var table, propNames, insertCell, row;

		if (agencies && agencies.length) {
			table = document.createElement("table");
			table.createTBody();
			// Get agency property names.
			propNames = Object.getOwnPropertyNames(agencies[0]);

			/** Inserts a cell into a row.
			 * @this {Object} Properties are row and agency.
			*/
			insertCell = function (propName, j) {
				var cell = this.row.insertCell(j);
				cell.textContent = String(this.agency[propName]);
			};

			// Add rows for each agency.
			agencies.forEach(function (agency, i) {
				var row = table.insertRow(i);
				propNames.forEach(insertCell, { row: row, agency: agency });
			});

			// Add the header
			table.createTHead();
			row = table.insertRow(0);

			propNames.forEach(function (propName, i) {
				var cell = row.insertCell(i);
				cell.textContent = propName;
			});
		}

		return table;

	}

	function handleAgencyList(/**{string}*/ json) {
		var agencies, agencyResponse = Agencies.parseAgencyResponse(json);

		/** Use with Array.filter() filter out non-WA agencies.
		*/

		if (agencyResponse.data) {
			agencies = agencyResponse.data;
			agencies = agencies.filter(isInWA);
			document.body.appendChild(createAgencyTable(agencies));
		}
	}

	function sendRequestForAgencyList() {
		var xhr = new XMLHttpRequest();
		xhr.open("GET", agencyListUrl, true);
		xhr.onload = function () {
			if (xhr.readyState === 4) {
				if (xhr.status === 200) {
					handleAgencyList(xhr.responseText);
				} else {
					console.error(xhr.statusText);
				}
			}
		};
		xhr.onerror = function (e) {
			console.error(e);
		};
		xhr.send();
	}

	sendRequestForAgencyList();
});<commit_msg>Changed table output.<commit_after>/*global require*/
require(["gtfs-exchange"], function (Agencies) {
	"use strict";
	var agencyListUrl = "proxy.ashx?http://www.gtfs-data-exchange.com/api/agencies";

	function isInWA(val) {
		return val && val.country === "United States" && val.state === "Washington";
	}

	function createAgencyTable(/**Object[]*/ agencies) {
		var table, propNames, insertCell, row;

		if (agencies && agencies.length) {
			table = document.createElement("table");
			table.createTBody();
			// Get agency property names.
			propNames = Object.getOwnPropertyNames(agencies[0]);

			/** Inserts a cell into a row.
			 * @this {Object} Properties are row and agency.
			*/
			insertCell = function (propName, j) {
				var urlRe = /url$/, dateRe = /date/, a, cb;
				var cell = this.row.insertCell(j);
				var value = this.agency[propName];
				if (urlRe.test(propName) && value) {
					a = document.createElement("a");
					a.textContent = "link";
					a.href = value;
					cell.appendChild(a);
				} else if(dateRe.test(propName)) {
					cell.textContent = value.toLocaleString();
				} else if (typeof value === "boolean") {
					cb = document.createElement("input");
					cb.type = "checkbox";
					cb.checked = value;
					cb.readOnly = true;
					cb.disabled = true;
					cell.appendChild(cb);
				} else {
					cell.textContent = String(value);
				}
			};

			// Add rows for each agency.
			agencies.forEach(function (agency, i) {
				var row = table.insertRow(i);
				if (!agency.is_official) {
					row.classList.add("unofficial");
				}
				propNames.forEach(insertCell, { row: row, agency: agency });
			});

			// Add the header
			table.createTHead();
			row = table.insertRow(0);

			propNames.forEach(function (propName, i) {
				var cell = row.insertCell(i);
				cell.textContent = propName;
			});
		}

		return table;

	}

	function handleAgencyList(/**{string}*/ json) {
		var agencies, agencyResponse = Agencies.parseAgencyResponse(json);

		/** Use with Array.filter() filter out non-WA agencies.
		*/

		if (agencyResponse.data) {
			agencies = agencyResponse.data;
			agencies = agencies.filter(isInWA);
			document.body.appendChild(createAgencyTable(agencies));
		}
	}

	function sendRequestForAgencyList() {
		var xhr = new XMLHttpRequest();
		xhr.open("GET", agencyListUrl, true);
		xhr.onload = function () {
			if (xhr.readyState === 4) {
				if (xhr.status === 200) {
					handleAgencyList(xhr.responseText);
				} else {
					console.error(xhr.statusText);
				}
			}
		};
		xhr.onerror = function (e) {
			console.error(e);
		};
		xhr.send();
	}

	sendRequestForAgencyList();
});<|endoftext|>


<commit_before>/*!
 * jCarousel Button Plugin v@VERSION
 * http://sorgalla.com/jcarousel/
 *
 * Copyright 2011, Jan Sorgalla
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * or GPL Version 2 (http://www.opensource.org/licenses/gpl-2.0.php) licenses.
 *
 * Date: @DATE
 */
(function($) {

    $.jcarousel.create('jcarousel.button', {
        options: {
            scroll: 1,
            event: 'click'
        },
        _init: function() {
            var self = this;

            this.carousel()
                .bind('jcarouseldestroy', function() {
                    self.destroy();
                })
                .bind('jcarouselreloadend jcarouselscrolltoend jcarouselscrollbyend', function() {
                    self.reload();
                });

            this.reload();
        },
        reload: function() {
            var carousel = this.carousel(),
                scroll   = this.option('scroll');

            this.element
                .unbind('.' + this._event)
                .bind(this.option('event') + '.' + this._event, function() {
                    if ($.data(this, 'jcarousel-button-enabled')) {
                        carousel.jcarousel('scrollBy', scroll);

                    }
                    return false;
                });

            var enabled = false,
                instance,
                wrap,
                items,
                size;

            carousel.each(function() {
                instance = $.data(this, 'jcarousel');
                wrap     = instance.option('wrap');
                items    = instance.items();
                size     = items.size();

                if (scroll > 0) {
                    enabled = size > 0 &&
                        ((wrap && wrap !== 'first') ||
                         (items.filter(':jcarousel-item-last').index() < (size - 1)) ||
                         (instance.tail && !instance.inTail)) ? true : false;
                } else {
                    enabled = size > 0 &&
                        ((wrap && wrap !== 'last') ||
                         (items.filter(':jcarousel-item-first').index() > 0) ||
                         (instance.tail && instance.inTail)) ? true : false;
                }

                if (enabled) {
                    return false;
                }
            });

            if (this.element.data('jcarousel-button-enabled') !== enabled) {
                this.element
                    .data('jcarousel-button-enabled',  enabled)
                    .data('jcarousel-button-disabled', !enabled)
                    .trigger('jcarouselbutton' + (enabled ? 'enabled' : 'disabled'));
            }

            return this;
        },
        destroy: function() {
            this.element
                .removeData(':jcarousel-button-enabled')
                .removeData(':jcarousel-button-disabled')
                .unbind('.' + this._event);
        }
    });

    $.expr.filters['jcarousel-button-enabled'] = function(elem) {
        return !!$.data(elem, 'jcarousel-button-enabled');
    };

    $.expr.filters['jcarousel-button-disabled'] = function(elem) {
        return !!$.data(elem, 'jcarousel-button-disabled');
    };

})(jQuery);
<commit_msg>Remove newline<commit_after>/*!
 * jCarousel Button Plugin v@VERSION
 * http://sorgalla.com/jcarousel/
 *
 * Copyright 2011, Jan Sorgalla
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * or GPL Version 2 (http://www.opensource.org/licenses/gpl-2.0.php) licenses.
 *
 * Date: @DATE
 */
(function($) {

    $.jcarousel.create('jcarousel.button', {
        options: {
            scroll: 1,
            event: 'click'
        },
        _init: function() {
            var self = this;

            this.carousel()
                .bind('jcarouseldestroy', function() {
                    self.destroy();
                })
                .bind('jcarouselreloadend jcarouselscrolltoend jcarouselscrollbyend', function() {
                    self.reload();
                });

            this.reload();
        },
        reload: function() {
            var carousel = this.carousel(),
                scroll   = this.option('scroll');

            this.element
                .unbind('.' + this._event)
                .bind(this.option('event') + '.' + this._event, function() {
                    if ($.data(this, 'jcarousel-button-enabled')) {
                        carousel.jcarousel('scrollBy', scroll);
                    }
                    return false;
                });

            var enabled = false,
                instance,
                wrap,
                items,
                size;

            carousel.each(function() {
                instance = $.data(this, 'jcarousel');
                wrap     = instance.option('wrap');
                items    = instance.items();
                size     = items.size();

                if (scroll > 0) {
                    enabled = size > 0 &&
                        ((wrap && wrap !== 'first') ||
                         (items.filter(':jcarousel-item-last').index() < (size - 1)) ||
                         (instance.tail && !instance.inTail)) ? true : false;
                } else {
                    enabled = size > 0 &&
                        ((wrap && wrap !== 'last') ||
                         (items.filter(':jcarousel-item-first').index() > 0) ||
                         (instance.tail && instance.inTail)) ? true : false;
                }

                if (enabled) {
                    return false;
                }
            });

            if (this.element.data('jcarousel-button-enabled') !== enabled) {
                this.element
                    .data('jcarousel-button-enabled',  enabled)
                    .data('jcarousel-button-disabled', !enabled)
                    .trigger('jcarouselbutton' + (enabled ? 'enabled' : 'disabled'));
            }

            return this;
        },
        destroy: function() {
            this.element
                .removeData(':jcarousel-button-enabled')
                .removeData(':jcarousel-button-disabled')
                .unbind('.' + this._event);
        }
    });

    $.expr.filters['jcarousel-button-enabled'] = function(elem) {
        return !!$.data(elem, 'jcarousel-button-enabled');
    };

    $.expr.filters['jcarousel-button-disabled'] = function(elem) {
        return !!$.data(elem, 'jcarousel-button-disabled');
    };

})(jQuery);
<|endoftext|>