<commit_before>// BeagleBone.js (c) 2014 Loren West
// May be freely distributed under the MIT license.
// For further details and documentation:
// http://lorenwest.github.com/beaglebone-monitor

var Monitor = require('monitor'),
    Probe = Monitor.Probe,
    Bonescript = require('bonescript'),
    bonePins = Bonescript.bone.pins,
    logger = Monitor.getLogger('BeagleBone');

/**
* This is a probe exposure for remote monitoring and control of the beaglebone.
*
* It allows you to define pin names, and interact with the beaglebone by your
* own pin names, and establish polling and real-time monitoring for input pins.
*
* The data model contains an element for each mapped pin (by name), and an
* element called pins, containing a hash of pin name to everything known about
* that pin.
*
* @class BeagleBone
* @constructor
*/
var BeagleBone = Probe.extend({

  probeClass: 'BeagleBone',
  initialize: function(params, options) {
    var t = this;

    // The pin map by ID keys on BeagleBone pin ID
    // key=>BeagleBone ID 'P9_22'  value=>Pin object
    t.pinMapById = {};

    // The pin map by name is exposed by the probe
    // key=>Pin name (as set by definePins), value=>Pin object
    t.pinMapByName = {};
    t.pinMapString = '';

    // key=>ms delay, value=>{interval:interval, reads:[array of pin names to read]}
    t.timerMap = {};

    // Set the initial data model
    t.updateModel({});
  },

  // This updates the data model in a single change.
  // @param updates {Object} Map of pin names to values
  updateModel: function(updates) {
    var t = this;

    // Update the whole pin map if necessary
    var pinMapString = JSON.stringify(t.pinMapByName);
    if (t.pinMapString !== pinMapString) {
      t.pinMapString = pinMapString;
      updates.pins = t.pinMapByName.slice(0);
    }

    // Update the data model
    t.set(updates);
  },

  /**
  * Define the controller pin mappings
  *
  * Pin mappings initialize the beaglebone I/O pins, and map them to your
  * own names.
  *
  * @method definePins_control - Remote pin mapping
  * @param pins {Array of Object} Initial I/O pin definitions
  * @param pins.n.name {String} The logical name to assign the pin
  * @param [pins.n.id] {String} The beaglebone ID of the pin ('P9_22')
  * @param [pins.n.type='manual'] {String} The pin type enumeration:
  *        gpio: The pin is mapped to a GPIO header
  *        analog: The pin is mapped to an analog input
  *        manual: Externally set by setPinValue_control
  * @param [pins.n.direction] {String} Pin direction ('in' or 'out')
  * @param [pins.n.value] {Mixed} Initial value to set the pin to
  * @param [pins.pull='pullup'] - (GPIO out) One of 'pullup', 'pulldown', or 'disabled'
  * @param [pins.slew='fast'] - (GPIO out) One of 'fast' or 'slow'
  * @param [pins.frequency=2000] - PWM frequency for digital output pins
  * @param [pins.n.precision=3] {Number} Number of digits to retain past the
  *        decimal point (rounded) for analog input pins.
  *        Dry contacts use 0 (for 0/1)
  * @param [pins.n.pollMs=1000] {Number} Number of milliseconds to poll for input values
  * @param callback {function(error)} - Called when done
  */
  definePins_control: function(pins, callback) {
    var t = this,
        pinsToInit = [],
        pinsToRead = [],
        pinsToWrite = [];

    // Perform validation before setting the pins
    pins.forEach(function(pin) {

      // Is this pin name already defined?
      if (t.pinMapByName[pin.name]) {
        return callback({code: 'NAME_IN_USE', msg: 'Pin name is already taken: ' + pin.name, pin: pin});
      }

      // Is this mapped to a beaglebone pin?
      if (pin.id) {

        // Is this a known pin?
        if (!bonePins[pin.id]) {
          return callback({code: 'UNKNOWN_PIN', msg: 'Pin id not found: ' + pin.id, pin: pin});
        }
        pin.bonePin = bonePins[pin.id];

        // Is this pin id already defined?
        if (t.pinMapById[pin.id]) {
          return callback({code: 'PIN_IN_USE', msg: 'Pin is already in use: ' + pin.id, pin: pin});
        }
      }

    });

    // Define each pin
    pins.forEach(function(pin) {

      // Add the pin definition to the maps
      t.pinMapByName[pin.name] = pin;
      if (pin.id) {
        t.pinMapById[pin.id] = pin;
      }

      // Process a GPIO pin
      if (pin.type === 'gpio') {
        pin.direction = pin.direction || 'in';
        if (!bonePin || !bonePin.gpio) {
          return callback({code:'NOT_GPIO_PIN', msg: 'Not an analog input type pin: ' + pin.name, pin: pin});
        }
        if (pin.direction === 'in') {
          pin.precision = 0;
        }
        else {
        }
      }

      // Process an analog pin
      if (pin.type === 'analog') {
        pin.direction = pin.direction || 'in';
        if (pin.direction === 'in') {
          // Analog input pin
          if (!bonePin || !bonePin.ain) {
            return callback({code:'NOT_ANALOG_IN', msg: 'Not an analog input type pin: ' + pin.name, pin: pin});
          }
          if (typeof pin.precision === 'undefined') {
            pin.precision = 3;
          }
        }
        else {
          // Analog output pin
          if (!bonePin || !bonePin.pwm) {
            return callback({code:'NOT_ANALOG_OUT', msg: 'Not an analog output (PWM) type pin: ' + pin.name, pin: pin});
          }
          if (typeof pin.frequency === 'undefined') {
            pin.frequency = 2000;
          }
        }
      }

    });

  },

  /**
  * Read a bunch of digital or analog pins
  *
  * @method read_control
  * @param pinNames {Array of String} Array of defined pin names to read
  * @param callback {function(error, valueMap)} - A map of pin name to pin values
  */
  read_control: function(pinNames, callback) {
    var t = this,
        didError = false,
        valueMap = {};

    // Convert to an array
    pinNames = Array.isArray(pinNames) ? pinNames : [pinNames];
    var numLeft = pinNames.length;
    if (pinNames.length === 0) {
      return callback(null,{});
    }

    // Validate the input
    pinNames.forEach(function(pinName) {
      var pin = t.pinMapByName[pinName];
      if (!pin) {
        return callback({code:'NO_PIN', msg:'No pin defined with name: ' + pinName});
      }
      if (pin.direction !== 'input') {
        return callback({code:'BAD_PIN', msg:'This isnt an input pin: ' + pinName});
      }
      if (pin.type !== 'analog' && pin.type !== 'digital') {
        return callback({code:'BAD_PIN_type', msg:'This isnt a digital or analog input pin: ' + pinName});
      }
    });

    // Called when done
    var whenDone = function(error) {
      if (didError) {
        return;
      }
      if (error) {
        didError = true;
        t.updateModel(valueMap);
        return callback(error);
      }
      if (--numLeft === 0) {
        t.updateModel(valueMap);
        return callback(null, valueMap);
      }
    }

    // Perform the read on each pin
    pinNames.forEach(function(pinName) {
      var pin = t.pinMapByName[pinName];
      var readFn = pin.type === 'analog' ? 'analogRead' : 'digitalRead';
      Bonescript[readFn](pin.id, function(x) {
        var error = x.err;
        var value = x.value;
        if (!error) {
          value = +value.toFixed(pin.precision);
          valueMap[pinName] = value;
        }
        whenDone(error);
      });
    });
  },

  /**
  * Write a bunch of pins, calling the callback when done
  *
  * @method write_control
  * @param pins {Object} Map of pins to write, name=pinName, value=value
  * @param callback {function(error)} - Called when complete
  */
  write_control: function(pins, callback) {
    var t = this,
        didError = false,
        allPins = [],
        valueMap = {};

    if (pins.length === 0) {
      return callback(null);
    }
    // Validate the input
    for (var pinName in pins) {
      var pin = t.pinMapByName[pinName];
      if (!pin) {
        return callback({code:'NO_PIN', msg:'No pin defined with name: ' + pinName});
      }
      if (pin.direction !== 'output') {
        return callback({code:'BAD_PIN', msg:'This isnt an output pin: ' + pinName});
      }
      if (pin.type !== 'analog' && pin.type !== 'gpio') {
        return callback({code:'BAD_PIN_TYPE', msg:'This isnt an analog or gpio pin: ' + pinName});
      }
      allPins.push(pinName);
    }

    // Called when done
    var whenDone = function(error) {
      if (didError) {
        return;
      }
      if (error) {
        didError = true;
        t.updateModel(valueMap);
        return callback(error);
      }
      if (--numLeft === 0) {
        t.updateModel(valueMap);
        return callback(null, valueMap);
      }
    }

    // Perform the write on each pin
    allPins.forEach(function(pinName) {
      var pin = t.pinMapByName[pinName];
      var value = pins[pinName];
      if (pin.type === 'analog') {
        Bonescript.analogWrite(pin.id, value, pin.frequency, function(x) {
          var error = x.err;
          if (!error) {
            valueMap[pinName] = value;
          }
          whenDone(error);
        });
      }
      else {
        Bonescript.digitalWrite(pin.id, value, function(x) {
          var error = x.err;
          if (!error) {
            valueMap[pinName] = value;
          }
          whenDone(error);
        });
      }
    });
  },

  /**
  * Set pin modes for a group of pins
  *
  * @method setPinModes
  * @param pins {Object array} An array of pin modes to set
  * @param pins.n.id {String} The beaglebone pin ID (P9_22)
  * @param pins.n.direction - One of b.INPUT or b.OUTPUT
  * @param [pins.n.mux=7] - Pin mux number (7 for gpio)
  * @param [pins.n.pull='pullup'] - One of 'pullup', 'pulldown', or 'disabled'
  * @param [pins.slew='fast'] - One of 'fast' or 'slow'
  * @param callback {function(error)} - Callback when complete
  */
  setPinModes: function(pins, callback) {
    var t = this;

    // Make an array, and default callback
    pins = Array.isArray(pins) ? pins : [pins];

    // Function for each pin
    var setPin = function(currentPin, cb) {
      var pin = pins[currentPin],
          mux = typeof pin.mux === 'undefined' ? 7 : pin.mux,
          pull = pin.pull ? pin.pull : 'pullup',
          slew = pin.slew ? pin.slew : 'fast';

      // Set the mode
      Bonescript.pinMode(pin.id, pin.direction, mux, pull, slew, function(err) {
        if (err && err.err) {
          return cb({err: err, msg: 'Error setting the pin mode for: ' + pin.id});
        }
        return cb(null);
      });
    };

    // Called when done with each pin
    var didError = false;
    var numLeft = pins.length;
    var whenDone = function(error) {
      if (didError) {
        return;
      }
      if (error) {
        didError = true;
        return callback(error);
      }
      if (--numLeft === 0) {
        callback();
      }
    }

    // Set each pin in parallel
    for (var i = 0; i < pins.length; i++) {
      setPin(i, whenDone);
    }
  },

  // Release resources held by the probe
  release: function() {
    var t = this;

    // Shut down all timers
    for (var i in t.timerMap) {
      var timer = t.timerMap[i];
      clearInterval(timer.interval);
    }
    t.timerMap = {};
  }

});
<commit_msg>Early checkin<commit_after>// BeagleBone.js (c) 2014 Loren West
// May be freely distributed under the MIT license.
// For further details and documentation:
// http://lorenwest.github.com/beaglebone-monitor

var Monitor = require('monitor'),
    logger = Monitor.getLogger('BeagleBone'),
    Probe = Monitor.Probe,
    Config = Monitor.Config,
    Bonescript = require('bonescript'),
    bonePins = Bonescript.bone.pins,
    logger = Monitor.getLogger('BeagleBone');

/**
* This is a probe exposure for remote monitoring and control of the beaglebone.
*
* It allows you to define pin names, and interact with the beaglebone by your
* own pin names, and establish polling and real-time monitoring for input pins.
*
* The data model contains an element for each mapped pin (by name), and an
* element called pins, containing a hash of pin name to everything known about
* that pin.
*
* @class BeagleBone
* @constructor
*/
var BeagleBone = Probe.extend({

  probeClass: 'BeagleBone',
  initialize: function(params, options) {
    var t = this;

    // The pin map by ID keys on BeagleBone pin ID
    // key=>BeagleBone ID 'P9_22'  value=>Pin object
    t.pinMapById = {};

    // The pin map by name is exposed by the probe
    // key=>Pin name (as set by definePins), value=>Pin object
    t.pinMapByName = {};
    t.pinMapString = '';

    // key=>ms delay, value=>{interval:interval, reads:[array of pin names to read]}
    t.timerMap = {};

    // Load any locally configured pins
    var pins = Config.BeagleBone && Config.BeagleBone ? Config.BeagleBone.pins : null;
    if (pins) {

      // Take over callback control
      options.asyncInit = true;

      // Configure the pins
      logger.info('init.localPins', 'Configuring local pins', pins);
      t.definePins_control(pins, options.callback);
    }

    // Set the initial data model
    t.updateModel({});
  },

  // This updates the data model in a single change.
  // @param updates {Object} Map of pin names to values
  updateModel: function(updates) {
    var t = this;

    // Update the whole pin map if necessary
    var pinMapString = JSON.stringify(t.pinMapByName);
    if (t.pinMapString !== pinMapString) {
      t.pinMapString = pinMapString;
      updates.pins = JSON.parse(JSON.stringify(t.pinMapByName));
    }

    // Update the data model
    t.set(updates);
  },

  /**
  * Define the controller pin mappings
  *
  * Pin mappings initialize the beaglebone I/O pins, and map them to your
  * own names.
  *
  * @method definePins_control - Remote pin mapping
  * @param pins {Array of Object} Initial I/O pin definitions
  * @param pins.n.name {String} The logical name to assign the pin
  * @param [pins.n.id] {String} The beaglebone ID of the pin ('P9_22')
  * @param [pins.n.type='manual'] {String} The pin type enumeration:
  *        gpio: The pin is mapped to a GPIO header
  *        analog: The pin is mapped to an analog input
  *        manual: Externally set by setPinValue_control
  * @param [pins.n.direction] {String} Pin direction ('in' or 'out')
  * @param [pins.n.value] {Mixed} Initial value to set the pin to
  * @param [pins.pull='pullup'] - (GPIO out) One of 'pullup', 'pulldown', or 'disabled'
  * @param [pins.slew='fast'] - (GPIO out) One of 'fast' or 'slow'
  * @param [pins.frequency=2000] - PWM frequency for digital output pins
  * @param [pins.n.precision=3] {Number} Number of digits to retain past the
  *        decimal point (rounded) for analog input pins.
  *        Dry contacts use 0 (for 0/1)
  * @param [pins.n.pollMs=1000] {Number} Number of milliseconds to poll for input values
  * @param callback {function(error)} - Called when done
  */
  definePins_control: function(pins, callback) {
    var t = this,
        pinsToInit = [],
        pinsToRead = [],
        pinsToWrite = [];

    // Perform validation before setting the pins
    pins.forEach(function(pin) {

      // Is this pin name already defined?
      if (t.pinMapByName[pin.name]) {
        return callback({code: 'NAME_IN_USE', msg: 'Pin name is already taken: ' + pin.name, pin: pin});
      }

      // Is this mapped to a beaglebone pin?
      if (pin.id) {

        // Is this a known pin?
        if (!bonePins[pin.id]) {
          return callback({code: 'UNKNOWN_PIN', msg: 'Pin id not found: ' + pin.id, pin: pin});
        }
        pin.bonePin = bonePins[pin.id];

        // Is this pin id already defined?
        if (t.pinMapById[pin.id]) {
          return callback({code: 'PIN_IN_USE', msg: 'Pin is already in use: ' + pin.id, pin: pin});
        }
      }

    });

    // Define each pin
    pins.forEach(function(pin) {

      // Add the pin definition to the maps
      t.pinMapByName[pin.name] = pin;
      if (pin.id) {
        t.pinMapById[pin.id] = pin;
      }

      // Process a GPIO pin
      if (pin.type === 'gpio') {
        pin.direction = pin.direction || 'in';
        if (!pin.bonePin || !pin.bonePin.gpio) {
          return callback({code:'NOT_GPIO_PIN', msg: 'Not an analog input type pin: ' + pin.name, pin: pin});
        }
        if (pin.direction === 'in') {
          pin.precision = 0;
        }
        else {
        }
      }

      // Process an analog pin
      if (pin.type === 'analog') {
        pin.direction = pin.direction || 'in';
        if (pin.direction === 'in') {
          // Analog input pin
          if (!pin.bonePin || !pin.bonePin.ain) {
            return callback({code:'NOT_ANALOG_IN', msg: 'Not an analog input type pin: ' + pin.name, pin: pin});
          }
          if (typeof pin.precision === 'undefined') {
            pin.precision = 3;
          }
        }
        else {
          // Analog output pin
          if (!pin.bonePin || !pin.bonePin.pwm) {
            return callback({code:'NOT_ANALOG_OUT', msg: 'Not an analog output (PWM) type pin: ' + pin.name, pin: pin});
          }
          if (typeof pin.frequency === 'undefined') {
            pin.frequency = 2000;
          }
        }
      }

    });

    // Initialize

    t.updateModel({});
    callback(null);

  },

  /**
  * Read a bunch of digital or analog pins
  *
  * @method read_control
  * @param pinNames {Array of String} Array of defined pin names to read
  * @param callback {function(error, valueMap)} - A map of pin name to pin values
  */
  read_control: function(pinNames, callback) {
    var t = this,
        didError = false,
        valueMap = {};

    // Convert to an array
    pinNames = Array.isArray(pinNames) ? pinNames : [pinNames];
    var numLeft = pinNames.length;
    if (pinNames.length === 0) {
      return callback(null,{});
    }

    // Validate the input
    pinNames.forEach(function(pinName) {
      var pin = t.pinMapByName[pinName];
      if (!pin) {
        return callback({code:'NO_PIN', msg:'No pin defined with name: ' + pinName});
      }
      if (pin.direction !== 'input') {
        return callback({code:'BAD_PIN', msg:'This isnt an input pin: ' + pinName});
      }
      if (pin.type !== 'analog' && pin.type !== 'digital') {
        return callback({code:'BAD_PIN_type', msg:'This isnt a digital or analog input pin: ' + pinName});
      }
    });

    // Called when done
    var whenDone = function(error) {
      if (didError) {
        return;
      }
      if (error) {
        didError = true;
        t.updateModel(valueMap);
        return callback(error);
      }
      if (--numLeft === 0) {
        t.updateModel(valueMap);
        return callback(null, valueMap);
      }
    }

    // Perform the read on each pin
    pinNames.forEach(function(pinName) {
      var pin = t.pinMapByName[pinName];
      var readFn = pin.type === 'analog' ? 'analogRead' : 'digitalRead';
      Bonescript[readFn](pin.id, function(x) {
        var error = x.err;
        var value = x.value;
        if (!error) {
          value = +value.toFixed(pin.precision);
          valueMap[pinName] = value;
        }
        whenDone(error);
      });
    });
  },

  /**
  * Write a bunch of pins, calling the callback when done
  *
  * @method write_control
  * @param pins {Object} Map of pins to write, name=pinName, value=value
  * @param callback {function(error)} - Called when complete
  */
  write_control: function(pins, callback) {
    var t = this,
        didError = false,
        allPins = [],
        valueMap = {};

    if (pins.length === 0) {
      return callback(null);
    }
    // Validate the input
    for (var pinName in pins) {
      var pin = t.pinMapByName[pinName];
      if (!pin) {
        return callback({code:'NO_PIN', msg:'No pin defined with name: ' + pinName});
      }
      if (pin.direction !== 'output') {
        return callback({code:'BAD_PIN', msg:'This isnt an output pin: ' + pinName});
      }
      if (pin.type !== 'analog' && pin.type !== 'gpio') {
        return callback({code:'BAD_PIN_TYPE', msg:'This isnt an analog or gpio pin: ' + pinName});
      }
      allPins.push(pinName);
    }

    // Called when done
    var whenDone = function(error) {
      if (didError) {
        return;
      }
      if (error) {
        didError = true;
        t.updateModel(valueMap);
        return callback(error);
      }
      if (--numLeft === 0) {
        t.updateModel(valueMap);
        return callback(null, valueMap);
      }
    }

    // Perform the write on each pin
    allPins.forEach(function(pinName) {
      var pin = t.pinMapByName[pinName];
      var value = pins[pinName];
      if (pin.type === 'analog') {
        Bonescript.analogWrite(pin.id, value, pin.frequency, function(x) {
          var error = x.err;
          if (!error) {
            valueMap[pinName] = value;
          }
          whenDone(error);
        });
      }
      else {
        Bonescript.digitalWrite(pin.id, value, function(x) {
          var error = x.err;
          if (!error) {
            valueMap[pinName] = value;
          }
          whenDone(error);
        });
      }
    });
  },

  /**
  * Set pin modes for a group of pins
  *
  * @method setPinModes
  * @param pins {Object array} An array of pin modes to set
  * @param pins.n.id {String} The beaglebone pin ID (P9_22)
  * @param pins.n.direction - One of b.INPUT or b.OUTPUT
  * @param [pins.n.mux=7] - Pin mux number (7 for gpio)
  * @param [pins.n.pull='pullup'] - One of 'pullup', 'pulldown', or 'disabled'
  * @param [pins.slew='fast'] - One of 'fast' or 'slow'
  * @param callback {function(error)} - Callback when complete
  */
  setPinModes: function(pins, callback) {
    var t = this;

    // Make an array, and default callback
    pins = Array.isArray(pins) ? pins : [pins];

    // Function for each pin
    var setPin = function(currentPin, cb) {
      var pin = pins[currentPin],
          mux = typeof pin.mux === 'undefined' ? 7 : pin.mux,
          pull = pin.pull ? pin.pull : 'pullup',
          slew = pin.slew ? pin.slew : 'fast';

      // Set the mode
      Bonescript.pinMode(pin.id, pin.direction, mux, pull, slew, function(err) {
        if (err && err.err) {
          return cb({err: err, msg: 'Error setting the pin mode for: ' + pin.id});
        }
        return cb(null);
      });
    };

    // Called when done with each pin
    var didError = false;
    var numLeft = pins.length;
    var whenDone = function(error) {
      if (didError) {
        return;
      }
      if (error) {
        didError = true;
        return callback(error);
      }
      if (--numLeft === 0) {
        callback();
      }
    }

    // Set each pin in parallel
    for (var i = 0; i < pins.length; i++) {
      setPin(i, whenDone);
    }
  },

  // Release resources held by the probe
  release: function() {
    var t = this;

    // Shut down all timers
    for (var i in t.timerMap) {
      var timer = t.timerMap[i];
      clearInterval(timer.interval);
    }
    t.timerMap = {};
  }

});
<|endoftext|>


<commit_before>var CompositeDisposable, ProjectDropdown,
    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
    $ = jQuery = require('jquery');

CompositeDisposable = require('atom').CompositeDisposable;

module.exports = ProjectDropdown = {
    subscriptions: null,
    changePathsObject: null,
    activate: function(state) {
        var scope = this,
            dropdownActive = false,
            $treeViewScroller = $('.tree-view-scroller');

        // Use window ready to identify when the tree view is loaded
        $(window).ready(function(){
            var existingPaths = atom.project.getPaths();

            if(existingPaths.length){
                scope.createDropdown();
                dropdownActive = true;
            }

            ProjectDropdown.changePathsObject = atom.project.onDidChangePaths(function(e){
                if(e.length){
                    if(!dropdownActive){
                        scope.createDropdown();
                        dropdownActive = true;
                    }

                    scope.createOptions(e.pop());
                }
            });
        });
    },
    createDropdown: function(){
        var $treeViewScroller = $('.tree-view-scroller'),
            $projectContainer = $('<div/>', {
                'class': 'project-dropdown-container'
            }),
            $selectElement = $('<select/>', {
                'class': 'project-dropdown-select',
                'id': 'project-dropdown-select'
            }),
            $closeButton = $('<button/>', {
                'class': 'project-dropdown-close'
            });

        // Create the project dropdown
        $projectContainer.prependTo($treeViewScroller);
        $selectElement.appendTo($projectContainer);
        $closeButton.appendTo($projectContainer);

        // Close active project on click
        $closeButton.on('click', ProjectDropdown.closeActiveProject);

        // Create the options
        ProjectDropdown.createOptions();

        // When the select box gets changed, update the project directory
        $selectElement.on('change', function(){
            ProjectDropdown.setActiveProject();
        });

        // Force the select box to follow you as you scroll
        $('.tree-view-scroller').scroll(function(){
            $('div.project-dropdown-container').css('top', $('.tree-view-scroller').scrollTop());
        });
    },
    setActiveProject: function(){
        // Find the current select value and make sure the project directory is reflected
        var $chosenOption = $('select.project-dropdown-select').find(':selected'),
            path = $chosenOption.data('path');

        $('.tree-view-scroller .directory.project-root').hide();
        $(".tree-view-scroller .directory.project-root .header.list-item span.name[data-path='" + path + "']").parents('li').show();
    },
    closeActiveProject: function(){
        // Find the current select value and make sure the project directory is reflected
        var $chosenOption = $('select.project-dropdown-select').find(':selected'),
            path = $chosenOption.data('path');

        // Remove directory from project
        atom.project.removePath(path);

        // Re-register options
        ProjectDropdown.createOptions();
    },
    createOptions: function(selectSpecific) {
        var $elements = [],
            $selectElement = $('select.project-dropdown-select');

        $('option', $selectElement).remove();

        // Create option elements
        $('.tree-view-scroller .directory.project-root').each(function(){
            var $this = $(this);
            var $option = $('<option/>', {
                'text': $('.header .name', $this).data('name'),
                'data-path': $this.find('.header.list-item span.name').data('path')
            });

            $elements.push($option);
        });

        // Re-organise the elements
        $elements.sort(function(a, b){
            // Convert values to uppercase to ensure fair sorting
            var textA = a[0].text.toUpperCase();
            var textB = b[0].text.toUpperCase();
            if(textA < textB) return -1;
            if(textA > textB) return 1;
            return 0;
        });

        // Loop through the elements and append them to the select box
        $.each($elements, function(index, value){
            value.appendTo($('select.project-dropdown-select'));
        });

        if(selectSpecific) {
            // Get the element of the new item
            var $newActive = $('span.name[data-path="' + selectSpecific + '"]').parents('li');
            // Hide all project roots
            $('.tree-view-scroller .directory.project-root').hide();
            // Show specific item
            $newActive.show();
            // Override the select to the new specific item
            $('select.project-dropdown-select').val($('select.project-dropdown-select').find('[data-path="' + selectSpecific + '"]').val());
        }else{
            ProjectDropdown.setActiveProject();
        }

        if(!$elements.length){
            $('.project-dropdown-container').hide();
        }else{
            $('.project-dropdown-container').show();
        }
    },
    deactivate: function() {
        $('.project-dropdown-container').remove();
        $('.tree-view-scroller .directory.project-root').show();
        ProjectDropdown.changePathsObject.dispose();
    }
};
<commit_msg>Fix Windows issue around project selection<commit_after>var CompositeDisposable, ProjectDropdown,
    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
    $ = jQuery = require('jquery');

CompositeDisposable = require('atom').CompositeDisposable;

module.exports = ProjectDropdown = {
    subscriptions: null,
    changePathsObject: null,
    activate: function(state) {
        var scope = this,
            dropdownActive = false,
            $treeViewScroller = $('.tree-view-scroller');

        // Use window ready to identify when the tree view is loaded
        $(window).ready(function(){
            var existingPaths = atom.project.getPaths();

            if(existingPaths.length){
                scope.createDropdown();
                dropdownActive = true;
            }

            ProjectDropdown.changePathsObject = atom.project.onDidChangePaths(function(e){
                if(e.length){
                    if(!dropdownActive){
                        scope.createDropdown();
                        dropdownActive = true;
                    }

                    scope.createOptions(e.pop());
                }
            });
        });
    },
    createDropdown: function(){
        var $treeViewScroller = $('.tree-view-scroller'),
            $projectContainer = $('<div/>', {
                'class': 'project-dropdown-container'
            }),
            $selectElement = $('<select/>', {
                'class': 'project-dropdown-select',
                'id': 'project-dropdown-select'
            }),
            $closeButton = $('<button/>', {
                'class': 'project-dropdown-close'
            });

        // Create the project dropdown
        $projectContainer.prependTo($treeViewScroller);
        $selectElement.appendTo($projectContainer);
        $closeButton.appendTo($projectContainer);

        // Close active project on click
        $closeButton.on('click', ProjectDropdown.closeActiveProject);

        // Create the options
        ProjectDropdown.createOptions();

        // When the select box gets changed, update the project directory
        $selectElement.on('change', function(){
            ProjectDropdown.setActiveProject();
        });

        // Force the select box to follow you as you scroll
        $('.tree-view-scroller').scroll(function(){
            $('div.project-dropdown-container').css('top', $('.tree-view-scroller').scrollTop());
        });
    },
    setActiveProject: function(){
        // Find the current select value and make sure the project directory is reflected
        var $chosenOption = $('select.project-dropdown-select').find(':selected'),
            path = $chosenOption.data('path').split(String.fromCharCode(92)).join(String.fromCharCode(92,92));

        $('.tree-view-scroller .directory.project-root').hide();
        $(".tree-view-scroller .directory.project-root .header.list-item span.name[data-path='" + path + "']").parents('li').show();
    },
    closeActiveProject: function(){
        // Find the current select value and make sure the project directory is reflected
        var $chosenOption = $('select.project-dropdown-select').find(':selected'),
            path = $chosenOption.data('path');

        // Remove directory from project
        atom.project.removePath(path);

        // Re-register options
        ProjectDropdown.createOptions();
    },
    createOptions: function(selectSpecific) {
        var $elements = [],
            $selectElement = $('select.project-dropdown-select');

        $('option', $selectElement).remove();

        // Create option elements
        $('.tree-view-scroller .directory.project-root').each(function(){
            var $this = $(this);
            var $option = $('<option/>', {
                'text': $('.header .name', $this).data('name'),
                'data-path': $this.find('.header.list-item span.name').data('path')
            });

            $elements.push($option);
        });

        // Re-organise the elements
        $elements.sort(function(a, b){
            // Convert values to uppercase to ensure fair sorting
            var textA = a[0].text.toUpperCase();
            var textB = b[0].text.toUpperCase();
            if(textA < textB) return -1;
            if(textA > textB) return 1;
            return 0;
        });

        // Loop through the elements and append them to the select box
        $.each($elements, function(index, value){
            value.appendTo($('select.project-dropdown-select'));
        });

        if(selectSpecific) {
          selectSpecific = selectSpecific.split(String.fromCharCode(92)).join(String.fromCharCode(92,92));
            // Get the element of the new item
            var $newActive = $('span.name[data-path="' + selectSpecific + '"]').parents('li');
            // Hide all project roots
            $('.tree-view-scroller .directory.project-root').hide();
            // Show specific item
            $newActive.show();
            // Override the select to the new specific item
            $('select.project-dropdown-select').val($('select.project-dropdown-select').find('[data-path="' + selectSpecific + '"]').val());
        }else{
            ProjectDropdown.setActiveProject();
        }

        if(!$elements.length){
            $('.project-dropdown-container').hide();
        }else{
            $('.project-dropdown-container').show();
        }
    },
    deactivate: function() {
        $('.project-dropdown-container').remove();
        $('.tree-view-scroller .directory.project-root').show();
        ProjectDropdown.changePathsObject.dispose();
    }
};
<|endoftext|>


<commit_before>var _ = require('underscore');
var Q = require('q');
var mongoose = require('mongoose-q')();
var util = require('../util');
var Quiz = require('../quiz').Quiz;
var FillInQuestion = require('../questions/fillIn').FillInQuestion;
var MultipleChoiceQuestion = require('../questions/multipleChoice').MultipleChoiceQuestion;
var Topic = require('../quiz').Topic;
var User = require('../user');
var Question = require('../../lib/question').Question;

/**
 * Creates a quiz and associates it with the given user and topic. Note that both a user and a topic
 * are required to create a quiz, and the topic must be owned by the user.
 * @param data Quiz data
 * @param user The user who should own this quiz
 * @returns An array containing:
 *  - The created topic.
 *  - The updated user document.
 */
exports.createQuiz = function (data, user) {
    if (!user) {
        throw new Error('user is required to create a quiz');
    }

    var ret = [];
    return Q(Topic.findById(data.topic).exec())
        .then(function (topic) {
            if (!topic) { throw new Error("Topic not found."); }
            if (!topic.createdBy.equals(user._id)) { throw new Error("Failed to create quiz: topic is not owned by user."); }
            return Quiz.createQ({
                name: data.name,
                topic: topic,
                dateCreated: new Date(),
                createdBy: user
            });
        })
        .then(function (quiz) {
            // Save the questions
            _.each(data.questions, function (question) {
                if (!(question instanceof Question)) {
                    question = exports.createQuestion(question);
                }
                quiz.questions.push(question);
            });
            return quiz.saveQ();
        }).then(function (quiz) {
            ret.push(quiz);
            user.quizzes.push(quiz);
            return user.saveQ();
        }).then(function () {
            ret.push(user);
            return ret;
        });
};

/**
 * Creates a topic and associates it with the given user.
 * @param data - Topic data. Should include the following properties:
 *  - name: The name of the topic. (Defaults to "New Topic".)
 *  - parent: Parent topic document, or null if this is a top-level topic. The parent topic
 *      must be owned by the same user.
 * @param user - The user who owns the topic.
 * @returns An array containing:
 *  - The created topic.
 *  - The updated user document.
 */
exports.createTopic = function (data, user) {
    if (!user) {
        return Q.reject("Invalid user");
    }

    // If creating a subtopic, verify that the parent topic is owned by the user.
    var promise = Q();
    var parentTopic;
    if (data.parent) {
        promise = Q(Topic.findById(data.parent._id || data.parent).exec())
            .then(function (parent) {
                parentTopic = parent;
                if (!parentTopic) {
                    throw new Error("Parent topic not found.");
                }
                if (!parentTopic.createdBy.equals(user._id)) {
                    throw new Error("Failed to create subtopic: parent topic not owned by user.")
                }
            });
    }

    // Create the topic
    var ret = [];
    return promise
        .then(function () {
            return Topic.createQ({
                name: data.name || "New Topic",
                dateCreated: new Date(),
                createdBy: user,
                parent: data.parent || null
            });
        })
        .then(function (topic) {
            ret.push(topic);
            if (data.parent) {
                // Add subtopics to the parent topic's subtopics array
                parentTopic.subtopics.push(topic);
                return parentTopic.saveQ().then(function () {
                    return user;
                });
            } else {
                // Add top-level topics to user.topics
                user.topics.push(topic);
                return user.saveQ();
            }
        })
        .then(function (user) {
            ret.push(user);
            return ret;
        });
};

/**
 * Given a plain object with a 'type' property, returns a proper Question instance, or throws an
 * error if the type is not valid.
 * @param question Plain object that has a 'type' property that identifies the type of question it
 *      represents, as well as all appropriate properties for that question type.
 * @returns {Question} An instance of the proper subclass of Question, depending on the question
 *      type.
 */
exports.createQuestion = function (question) {
    switch (question.type) {
        case 'FillInQuestion':
            return new FillInQuestion(question);
        case 'MultipleChoiceQuestion':
            return new MultipleChoiceQuestion(question);
        default:
            throw new Error('Invalid question type: ' + question.type);
    }
};

exports.getSubtopics = function (topic) {
    return Topic.find({ parent: topic._id }).exec();
};

exports.getQuizzesByTopic = function (topic) {
    return Quiz.find({ topic: topic._id }).exec();
};

/**
 * Loads all the quizzes and topics.
 * @param user THe user whose quizzes and topics to load
 * @returns {Promise} Promise for an array of topics, with each topic having a subtopics property
 *      (for child topics) and a quizzes property (quizzes that are directly associated with that
 *      topic).
 */
exports.getQuizzesAndTopics = function (user) {
  // Load root-level topics
    return Q.all(_.map(user.topics, function (topicId) {
        return Topic.findById(topicId).exec();
    })).then(function (topics) {
        // Emit warnings for topics that were not found in the topics collection (implying
        // user.topics is out of sync).
        _.each(topics, function (topic, i) {
            if (!topic) {
                console.warn('Topic ' + user.topics[i] + ' exists in user.topics for user ' + user.email +
                    ' (' + user._id + '), but could not be found in topics collection.');
                return null;
            }
            return topic;
        });

        // Filter out null topics
        topics = _.without(topics, null);

        // Transform root topics into simple objects, and strip off unneeded properties
        var result = _.map(topics, function (topic) {
            return _.omit(topic.toObject(), [ 'createdBy', '__v' ]);
        });

        // Recursively load quizzes and subtopics.
        return Q.all(_.map(topics, function (topic, i) {
            return exports.getQuizzesAndSubtopics(topic, result[i]);
        })).then(function () {
            return result;
        });
    });
};

/**
 * Returns the quizzes and child topics associated with the given topic. The quizzes and subtopics
 * are returned by modifying the 'result' parameter.
 * @param topic The topic whose quizzes and subtopics to load.
 * @param result The object where to tack on the quizzes and subtopics. This object will be modified
 *      by adding a "quizzes" property and a "subtopics" property.
 * @returns {Promise} This function returns the passed in result object with the quizzes and
 *      subtopics tacked onto it.
 */
exports.getQuizzesAndSubtopics = function (topic, result) {
    return Q.all([exports.getQuizzesByTopic(topic), exports.getSubtopics(topic)])
        .spread(function (quizzes, subtopics) {
            result.quizzes = _.map(quizzes, function (quiz) {
                quiz = quiz.toObject();
                quiz.numQuestions = quiz.questions.length;
                return _.omit(quiz, [ 'questions', 'createdBy', '__v' ]);
            });
            result.subtopics = _.map(subtopics, function (subtopic) {
                return _.omit(subtopic.toObject(), [ 'createdBy', '__v' ]);
            });
            return Q.all(_.map(subtopics, function (subtopic, i) {
                return exports.getQuizzesAndSubtopics(subtopic, result.subtopics[i]);
            })).then(function () {
                return result;
            });
        });
};

/**
 * Deletes the specified topic, along with its quizzes and all subtopics (and also including all
 * quizzes within all subtopics).
 * @param topic The topic to delete
 * @returns {Promise} Returns a promise for the deleted topic document.
 */
exports.deleteTopic = function (topic) {
    return exports.getQuizzesAndSubtopics(topic, topic.toObject())
        .then(function (result) {
            // Delete quizzes and subtopics
            return Q.all(_.map(result.quizzes, exports.deleteQuiz)
                .concat(_.map(result.subtopics, function (subtopic) {
                    return Topic.findById(subtopic._id).exec().then(function (subtopic) {
                        return exports.deleteTopic(subtopic);
                    });
                })));
        })
        .then(function () {
            // Delete the topic itself
            return Topic.findById(topic._id).exec().then(function (t) {
                return t.removeQ();
            });
        })
        .then(function (deletedTopic) {
            // Remove the topic from the user.topics array if it's a root topic.
            if (!deletedTopic.parent) {
                return User.findById(deletedTopic.createdBy).exec()
                    .then(function (user) {
                        return User.findByIdAndUpdate(user._id, { $pull: { topics: deletedTopic._id }}).exec();
                    })
                    .then(function () {
                        return deletedTopic;
                    });
            }
        });
};

exports.deleteQuiz = function (quiz) {
    return Quiz.findByIdAndRemove(quiz._id).exec()
        .then(function (quiz) {
            return User.findById(quiz.createdBy).exec();
        })
        .then(function (user) {
            return user.quizzes.remove(quiz._id);
        });
};

/**
 * Adds the topic tree, including all associated subtopics and quizzes, to the user's own list of
 * topics.
 * @param user The user who should own the imported topics and quizzes
 * @param topics An array of topic objects, which each topic having 'name', 'quizzes', and 'subtopics'
 *      properties.
 * @param root The root topic underneath which the imported topics should be added, or null if the
 *      topics should be imported as top-level topics.
 * @return {Promise}
 */
exports.importTopicTree = function (user, topics, root) {

    // TODO: Consider having this function return something useful, like the updated user doc, or
    // the topic hierarchy

    // TODO: Expose importTopic as a module export
    var importTopic = function importTopic (topic, parent) {
        topic.parent = parent;
        var createdTopic;
        return exports.createTopic(topic, user)
            .then(function (result) {
                createdTopic = result[0];
                user = result[1];

                var importRemainingQuizzes = function (remaining) {
                    var quiz = remaining.shift();
                    if (quiz) {
                        quiz.topic = createdTopic;
                        return exports.createQuiz(quiz, user).then(function (result) {
                            user = result[1];
                            return importRemainingQuizzes(remaining);
                        });
                    }
                };

                return Q(importRemainingQuizzes(topic.quizzes || []));
            })
            .then(function () {
                // Import topics in serial (to preserve original order)
                topic.subtopics = topic.subtopics || [];
                return topic.subtopics.reduce(function (promise, subtopic) {
                    return promise.then(function () {
                        return importTopic(subtopic, createdTopic);
                    });
                }, Q());
            });
    };

    // Argh! Root topics must be imported serially because they all modify the user document
    // (by pushing to the user.topics array), so we can't use Q.all here.
    //
    // TODO: It should be possible to clean this up a bit by either combining importTopic and
    // importRemainingTopics into one function, or maybe have importTopicTree itself be recursive
    // and have it import one topic at a time.
    var importRemainingTopics = function (remaining) {
        var topic = remaining.shift();
        if (topic) {
            return importTopic(topic, root).then(function () {
                return importRemainingTopics(remaining);
            });
        }
    };

    return importRemainingTopics(topics);
};

/**
 * Searches the given topic hierarchy for topics with the given name. Returns an array of topic
 * objects that match. Note: This method does not support partial matches at the moment, and the
 * matches are case-sensitive.
 * @param hierarchy The topic hierarchy to search (as returned by getQuizzesAndTopics)
 * @param name The name of the topic to search for (case-sensitive, exact match)
 * @return Array of topic objects with matching name
 */
exports.searchHierarchyByName = function (hierarchy, name) {
    var results = [];

    (function search (root) {
        var subtopics = root.subtopics || [];
        for (var j = 0; j < subtopics.length; j++) {
            if (subtopics[j].name === name) {
                results.push(subtopics[j]);
            }
            search(subtopics[j]);
        }
    })({ subtopics: hierarchy });

    return results;
};

exports.updateTopicPosition = function (topic, position) {
    if (topic.parent) {
        return Q(Topic.findById(topic.parent).exec())
            .then(function (parent) {
                var oldPos = parent.subtopics.indexOf(topic);
                parent.subtopics.splice(position, 0, parent.subtopics.splice(oldPos, 1)[0]);
                return parent.saveQ();
            });
    } else {
        // TODO
    }
}
<commit_msg>Implement QuizService.updateTopicPosition for root topics, and add a minor fix and more error handling for reordering subtopics<commit_after>var _ = require('underscore');
var Q = require('q');
var mongoose = require('mongoose-q')();
var util = require('../util');
var Quiz = require('../quiz').Quiz;
var FillInQuestion = require('../questions/fillIn').FillInQuestion;
var MultipleChoiceQuestion = require('../questions/multipleChoice').MultipleChoiceQuestion;
var Topic = require('../quiz').Topic;
var User = require('../user');
var Question = require('../../lib/question').Question;

/**
 * Creates a quiz and associates it with the given user and topic. Note that both a user and a topic
 * are required to create a quiz, and the topic must be owned by the user.
 * @param data Quiz data
 * @param user The user who should own this quiz
 * @returns An array containing:
 *  - The created topic.
 *  - The updated user document.
 */
exports.createQuiz = function (data, user) {
    if (!user) {
        throw new Error('user is required to create a quiz');
    }

    var ret = [];
    return Q(Topic.findById(data.topic).exec())
        .then(function (topic) {
            if (!topic) { throw new Error("Topic not found."); }
            if (!topic.createdBy.equals(user._id)) { throw new Error("Failed to create quiz: topic is not owned by user."); }
            return Quiz.createQ({
                name: data.name,
                topic: topic,
                dateCreated: new Date(),
                createdBy: user
            });
        })
        .then(function (quiz) {
            // Save the questions
            _.each(data.questions, function (question) {
                if (!(question instanceof Question)) {
                    question = exports.createQuestion(question);
                }
                quiz.questions.push(question);
            });
            return quiz.saveQ();
        }).then(function (quiz) {
            ret.push(quiz);
            user.quizzes.push(quiz);
            return user.saveQ();
        }).then(function () {
            ret.push(user);
            return ret;
        });
};

/**
 * Creates a topic and associates it with the given user.
 * @param data - Topic data. Should include the following properties:
 *  - name: The name of the topic. (Defaults to "New Topic".)
 *  - parent: Parent topic document, or null if this is a top-level topic. The parent topic
 *      must be owned by the same user.
 * @param user - The user who owns the topic.
 * @returns An array containing:
 *  - The created topic.
 *  - The updated user document.
 */
exports.createTopic = function (data, user) {
    if (!user) {
        return Q.reject("Invalid user");
    }

    // If creating a subtopic, verify that the parent topic is owned by the user.
    var promise = Q();
    var parentTopic;
    if (data.parent) {
        promise = Q(Topic.findById(data.parent._id || data.parent).exec())
            .then(function (parent) {
                parentTopic = parent;
                if (!parentTopic) {
                    throw new Error("Parent topic not found.");
                }
                if (!parentTopic.createdBy.equals(user._id)) {
                    throw new Error("Failed to create subtopic: parent topic not owned by user.")
                }
            });
    }

    // Create the topic
    var ret = [];
    return promise
        .then(function () {
            return Topic.createQ({
                name: data.name || "New Topic",
                dateCreated: new Date(),
                createdBy: user,
                parent: data.parent || null
            });
        })
        .then(function (topic) {
            ret.push(topic);
            if (data.parent) {
                // Add subtopics to the parent topic's subtopics array
                parentTopic.subtopics.push(topic);
                return parentTopic.saveQ().then(function () {
                    return user;
                });
            } else {
                // Add top-level topics to user.topics
                user.topics.push(topic);
                return user.saveQ();
            }
        })
        .then(function (user) {
            ret.push(user);
            return ret;
        });
};

/**
 * Given a plain object with a 'type' property, returns a proper Question instance, or throws an
 * error if the type is not valid.
 * @param question Plain object that has a 'type' property that identifies the type of question it
 *      represents, as well as all appropriate properties for that question type.
 * @returns {Question} An instance of the proper subclass of Question, depending on the question
 *      type.
 */
exports.createQuestion = function (question) {
    switch (question.type) {
        case 'FillInQuestion':
            return new FillInQuestion(question);
        case 'MultipleChoiceQuestion':
            return new MultipleChoiceQuestion(question);
        default:
            throw new Error('Invalid question type: ' + question.type);
    }
};

exports.getSubtopics = function (topic) {
    return Topic.find({ parent: topic._id }).exec();
};

exports.getQuizzesByTopic = function (topic) {
    return Quiz.find({ topic: topic._id }).exec();
};

/**
 * Loads all the quizzes and topics.
 * @param user THe user whose quizzes and topics to load
 * @returns {Promise} Promise for an array of topics, with each topic having a subtopics property
 *      (for child topics) and a quizzes property (quizzes that are directly associated with that
 *      topic).
 */
exports.getQuizzesAndTopics = function (user) {
  // Load root-level topics
    return Q.all(_.map(user.topics, function (topicId) {
        return Topic.findById(topicId).exec();
    })).then(function (topics) {
        // Emit warnings for topics that were not found in the topics collection (implying
        // user.topics is out of sync).
        _.each(topics, function (topic, i) {
            if (!topic) {
                console.warn('Topic ' + user.topics[i] + ' exists in user.topics for user ' + user.email +
                    ' (' + user._id + '), but could not be found in topics collection.');
                return null;
            }
            return topic;
        });

        // Filter out null topics
        topics = _.without(topics, null);

        // Transform root topics into simple objects, and strip off unneeded properties
        var result = _.map(topics, function (topic) {
            return _.omit(topic.toObject(), [ 'createdBy', '__v' ]);
        });

        // Recursively load quizzes and subtopics.
        return Q.all(_.map(topics, function (topic, i) {
            return exports.getQuizzesAndSubtopics(topic, result[i]);
        })).then(function () {
            return result;
        });
    });
};

/**
 * Returns the quizzes and child topics associated with the given topic. The quizzes and subtopics
 * are returned by modifying the 'result' parameter.
 * @param topic The topic whose quizzes and subtopics to load.
 * @param result The object where to tack on the quizzes and subtopics. This object will be modified
 *      by adding a "quizzes" property and a "subtopics" property.
 * @returns {Promise} This function returns the passed in result object with the quizzes and
 *      subtopics tacked onto it.
 */
exports.getQuizzesAndSubtopics = function (topic, result) {
    return Q.all([exports.getQuizzesByTopic(topic), exports.getSubtopics(topic)])
        .spread(function (quizzes, subtopics) {
            result.quizzes = _.map(quizzes, function (quiz) {
                quiz = quiz.toObject();
                quiz.numQuestions = quiz.questions.length;
                return _.omit(quiz, [ 'questions', 'createdBy', '__v' ]);
            });
            result.subtopics = _.map(subtopics, function (subtopic) {
                return _.omit(subtopic.toObject(), [ 'createdBy', '__v' ]);
            });
            return Q.all(_.map(subtopics, function (subtopic, i) {
                return exports.getQuizzesAndSubtopics(subtopic, result.subtopics[i]);
            })).then(function () {
                return result;
            });
        });
};

/**
 * Deletes the specified topic, along with its quizzes and all subtopics (and also including all
 * quizzes within all subtopics).
 * @param topic The topic to delete
 * @returns {Promise} Returns a promise for the deleted topic document.
 */
exports.deleteTopic = function (topic) {
    return exports.getQuizzesAndSubtopics(topic, topic.toObject())
        .then(function (result) {
            // Delete quizzes and subtopics
            return Q.all(_.map(result.quizzes, exports.deleteQuiz)
                .concat(_.map(result.subtopics, function (subtopic) {
                    return Topic.findById(subtopic._id).exec().then(function (subtopic) {
                        return exports.deleteTopic(subtopic);
                    });
                })));
        })
        .then(function () {
            // Delete the topic itself
            return Topic.findById(topic._id).exec().then(function (t) {
                return t.removeQ();
            });
        })
        .then(function (deletedTopic) {
            // Remove the topic from the user.topics array if it's a root topic.
            if (!deletedTopic.parent) {
                return User.findById(deletedTopic.createdBy).exec()
                    .then(function (user) {
                        return User.findByIdAndUpdate(user._id, { $pull: { topics: deletedTopic._id }}).exec();
                    })
                    .then(function () {
                        return deletedTopic;
                    });
            }
        });
};

exports.deleteQuiz = function (quiz) {
    return Quiz.findByIdAndRemove(quiz._id).exec()
        .then(function (quiz) {
            return User.findById(quiz.createdBy).exec();
        })
        .then(function (user) {
            return user.quizzes.remove(quiz._id);
        });
};

/**
 * Adds the topic tree, including all associated subtopics and quizzes, to the user's own list of
 * topics.
 * @param user The user who should own the imported topics and quizzes
 * @param topics An array of topic objects, which each topic having 'name', 'quizzes', and 'subtopics'
 *      properties.
 * @param root The root topic underneath which the imported topics should be added, or null if the
 *      topics should be imported as top-level topics.
 * @return {Promise}
 */
exports.importTopicTree = function (user, topics, root) {

    // TODO: Consider having this function return something useful, like the updated user doc, or
    // the topic hierarchy

    // TODO: Expose importTopic as a module export
    var importTopic = function importTopic (topic, parent) {
        topic.parent = parent;
        var createdTopic;
        return exports.createTopic(topic, user)
            .then(function (result) {
                createdTopic = result[0];
                user = result[1];

                var importRemainingQuizzes = function (remaining) {
                    var quiz = remaining.shift();
                    if (quiz) {
                        quiz.topic = createdTopic;
                        return exports.createQuiz(quiz, user).then(function (result) {
                            user = result[1];
                            return importRemainingQuizzes(remaining);
                        });
                    }
                };

                return Q(importRemainingQuizzes(topic.quizzes || []));
            })
            .then(function () {
                // Import topics in serial (to preserve original order)
                topic.subtopics = topic.subtopics || [];
                return topic.subtopics.reduce(function (promise, subtopic) {
                    return promise.then(function () {
                        return importTopic(subtopic, createdTopic);
                    });
                }, Q());
            });
    };

    // Argh! Root topics must be imported serially because they all modify the user document
    // (by pushing to the user.topics array), so we can't use Q.all here.
    //
    // TODO: It should be possible to clean this up a bit by either combining importTopic and
    // importRemainingTopics into one function, or maybe have importTopicTree itself be recursive
    // and have it import one topic at a time.
    var importRemainingTopics = function (remaining) {
        var topic = remaining.shift();
        if (topic) {
            return importTopic(topic, root).then(function () {
                return importRemainingTopics(remaining);
            });
        }
    };

    return importRemainingTopics(topics);
};

/**
 * Searches the given topic hierarchy for topics with the given name. Returns an array of topic
 * objects that match. Note: This method does not support partial matches at the moment, and the
 * matches are case-sensitive.
 * @param hierarchy The topic hierarchy to search (as returned by getQuizzesAndTopics)
 * @param name The name of the topic to search for (case-sensitive, exact match)
 * @return Array of topic objects with matching name
 */
exports.searchHierarchyByName = function (hierarchy, name) {
    var results = [];

    (function search (root) {
        var subtopics = root.subtopics || [];
        for (var j = 0; j < subtopics.length; j++) {
            if (subtopics[j].name === name) {
                results.push(subtopics[j]);
            }
            search(subtopics[j]);
        }
    })({ subtopics: hierarchy });

    return results;
};

exports.updateTopicPosition = function (topic, position) {
    if (topic.parent) {
        return Q(Topic.findById(topic.parent).exec())
            .then(function (parent) {
                var oldPos = parent.subtopics.indexOf(topic._id);
                if (oldPos === -1) {
                    throw new Error("Topic \"" + topic.name + "\" (" + topic._id.toString() +
                        ") is not a subtopic of \"" + parent.name + "\" (" + parent._id.toString() + ").");
                }
                parent.subtopics.splice(position, 0, parent.subtopics.splice(oldPos, 1)[0]);
                return parent.saveQ();
            });
    } else {
        return Q(User.findById(topic.createdBy).exec())
            .then(function (user) {
                var oldPos = user.topics.indexOf(topic._id);
                if (oldPos === -1) {
                    throw new Error("Topic \"" + topic.name + "\" (" + topic._id.toString() +
                        ") has no parent (indicating that it is a root topic), but it could not be " +
                        "found in the user.topics array.");
                }
                user.topics.splice(position, 0, user.topics.splice(oldPos, 1)[0]);
                return user.saveQ();
            });
    }
}
<|endoftext|>


<commit_before>"use strict";

const $Ref = require("./ref");
const Pointer = require("./pointer");
const parse = require("./parse");
const url = require("./util/url");
const { isHandledError } = require("./util/errors");

module.exports = resolveExternal;

/**
 * Crawls the JSON schema, finds all external JSON references, and resolves their values.
 * This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}.
 *
 * NOTE: We only care about EXTERNAL references here. INTERNAL references are only relevant when dereferencing.
 *
 * @param {$RefParser} parser
 * @param {$RefParserOptions} options
 *
 * @returns {Promise}
 * The promise resolves once all JSON references in the schema have been resolved,
 * including nested references that are contained in externally-referenced files.
 */
function resolveExternal (parser, options) {
  if (!options.resolve.external) {
    // Nothing to resolve, so exit early
    return Promise.resolve();
  }

  try {
    // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
    let promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs, options);
    return Promise.all(promises);
  }
  catch (e) {
    return Promise.reject(e);
  }
}

/**
 * Recursively crawls the given value, and resolves any external JSON references.
 *
 * @param {*} obj - The value to crawl. If it's not an object or array, it will be ignored.
 * @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash
 * @param {$Refs} $refs
 * @param {$RefParserOptions} options
 * @param {WeakMap} seen - Internal.
 *
 * @returns {Promise[]}
 * Returns an array of promises. There will be one promise for each JSON reference in `obj`.
 * If `obj` does not contain any JSON references, then the array will be empty.
 * If any of the JSON references point to files that contain additional JSON references,
 * then the corresponding promise will internally reference an array of promises.
 */
function crawl (obj, path, $refs, options, seen) {
  seen = seen || new WeakMap();
  let promises = [];

  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
    seen.set(obj, 1); // Track previously seen objects to avoid infinite recursion
    if ($Ref.isExternal$Ref(obj)) {
      promises.push(resolve$Ref(obj, path, $refs, options));
    }
    else {
      for (let key of Object.keys(obj)) {
        let keyPath = Pointer.join(path, key);
        let value = obj[key];

        if ($Ref.isExternal$Ref(value)) {
          promises.push(resolve$Ref(value, keyPath, $refs, options));
        }
        else {
          promises = promises.concat(crawl(value, keyPath, $refs, options, seen));
        }
      }
    }
  }

  return promises;
}

/**
 * Resolves the given JSON Reference, and then crawls the resulting value.
 *
 * @param {{$ref: string}} $ref - The JSON Reference to resolve
 * @param {string} path - The full path of `$ref`, possibly with a JSON Pointer in the hash
 * @param {$Refs} $refs
 * @param {$RefParserOptions} options
 *
 * @returns {Promise}
 * The promise resolves once all JSON references in the object have been resolved,
 * including nested references that are contained in externally-referenced files.
 */
async function resolve$Ref ($ref, path, $refs, options) {
  // console.log('Resolving $ref pointer "%s" at %s', $ref.$ref, path);

  let resolvedPath = url.resolve(path, $ref.$ref);
  let withoutHash = url.stripHash(resolvedPath);

  // Do we already have this $ref?
  $ref = $refs._$refs[withoutHash];
  if ($ref) {
    // We've already parsed this $ref, so use the existing value
    return Promise.resolve($ref.value);
  }

  // Parse the $referenced file/url
  try {
    const result = await parse(resolvedPath, $refs, options);

    // Crawl the parsed value
    // console.log('Resolving $ref pointers in %s', withoutHash);
    let promises = crawl(result, withoutHash + "#", $refs, options);

    return Promise.all(promises);
  }
  catch (err) {
    if (!options.continueOnError || !isHandledError(err)) {
      throw err;
    }

    if ($refs._$refs[withoutHash]) {
      err.source = url.stripHash(path);
      err.path = url.safePointerToPath(url.getHash(path));
    }

    return [];
  }
}
<commit_msg>just use a Set<commit_after>"use strict";

const $Ref = require("./ref");
const Pointer = require("./pointer");
const parse = require("./parse");
const url = require("./util/url");
const { isHandledError } = require("./util/errors");

module.exports = resolveExternal;

/**
 * Crawls the JSON schema, finds all external JSON references, and resolves their values.
 * This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}.
 *
 * NOTE: We only care about EXTERNAL references here. INTERNAL references are only relevant when dereferencing.
 *
 * @param {$RefParser} parser
 * @param {$RefParserOptions} options
 *
 * @returns {Promise}
 * The promise resolves once all JSON references in the schema have been resolved,
 * including nested references that are contained in externally-referenced files.
 */
function resolveExternal (parser, options) {
  if (!options.resolve.external) {
    // Nothing to resolve, so exit early
    return Promise.resolve();
  }

  try {
    // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
    let promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs, options);
    return Promise.all(promises);
  }
  catch (e) {
    return Promise.reject(e);
  }
}

/**
 * Recursively crawls the given value, and resolves any external JSON references.
 *
 * @param {*} obj - The value to crawl. If it's not an object or array, it will be ignored.
 * @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash
 * @param {$Refs} $refs
 * @param {$RefParserOptions} options
 * @param {Set} seen - Internal.
 *
 * @returns {Promise[]}
 * Returns an array of promises. There will be one promise for each JSON reference in `obj`.
 * If `obj` does not contain any JSON references, then the array will be empty.
 * If any of the JSON references point to files that contain additional JSON references,
 * then the corresponding promise will internally reference an array of promises.
 */
function crawl (obj, path, $refs, options, seen) {
  seen = seen || new Set();
  let promises = [];

  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
    seen.add(obj); // Track previously seen objects to avoid infinite recursion
    if ($Ref.isExternal$Ref(obj)) {
      promises.push(resolve$Ref(obj, path, $refs, options));
    }
    else {
      for (let key of Object.keys(obj)) {
        let keyPath = Pointer.join(path, key);
        let value = obj[key];

        if ($Ref.isExternal$Ref(value)) {
          promises.push(resolve$Ref(value, keyPath, $refs, options));
        }
        else {
          promises = promises.concat(crawl(value, keyPath, $refs, options, seen));
        }
      }
    }
  }

  return promises;
}

/**
 * Resolves the given JSON Reference, and then crawls the resulting value.
 *
 * @param {{$ref: string}} $ref - The JSON Reference to resolve
 * @param {string} path - The full path of `$ref`, possibly with a JSON Pointer in the hash
 * @param {$Refs} $refs
 * @param {$RefParserOptions} options
 *
 * @returns {Promise}
 * The promise resolves once all JSON references in the object have been resolved,
 * including nested references that are contained in externally-referenced files.
 */
async function resolve$Ref ($ref, path, $refs, options) {
  // console.log('Resolving $ref pointer "%s" at %s', $ref.$ref, path);

  let resolvedPath = url.resolve(path, $ref.$ref);
  let withoutHash = url.stripHash(resolvedPath);

  // Do we already have this $ref?
  $ref = $refs._$refs[withoutHash];
  if ($ref) {
    // We've already parsed this $ref, so use the existing value
    return Promise.resolve($ref.value);
  }

  // Parse the $referenced file/url
  try {
    const result = await parse(resolvedPath, $refs, options);

    // Crawl the parsed value
    // console.log('Resolving $ref pointers in %s', withoutHash);
    let promises = crawl(result, withoutHash + "#", $refs, options);

    return Promise.all(promises);
  }
  catch (err) {
    if (!options.continueOnError || !isHandledError(err)) {
      throw err;
    }

    if ($refs._$refs[withoutHash]) {
      err.source = url.stripHash(path);
      err.path = url.safePointerToPath(url.getHash(path));
    }

    return [];
  }
}
<|endoftext|>


<commit_before>'use strict';

var ddb = require('../../config/dynamo_database').ddb;
var _ = require('underscore');

var proposalModel = require('../model/proposalModel');

exports.findAll = function (req, res){
  return ddb.scan('proposals', {}, function (err, proposals) {
    if (!err) {
      return res.send(_.sortBy(proposals.items, function(element){
        return element.tema + element.subtema;
      }) );
    } else {
      return console.log(err);
    }
  });
};

exports.add = function (req, res){
  var proposal = proposalModel.newProposal(req);
  ddb.putItem('proposals', proposal, {}, function (err) {
    if (!err) {
      return console.log("Created Proposal");
    } else {
      return console.log(err);
    }
  });
  return res.send(202,proposal);
};

exports.findById = function (req, res){
  return ddb.getItem('proposals', req.params.id, null, {}, function (err, proposal) {
    if (!err) {
      return res.send(proposal);
    } else {
      return console.log(err);
    }
  });
};

exports.update = function (req, res){
  var proposal = proposalModel.updateProposal(req);

  return ddb.updateItem('proposals', req.params.id, null, proposal, function (err, proposals) {
      if (!err) {
        console.log("Updated Proposal");
      } else {
        console.log(err);
      }
      return res.send(202, proposals);
  });
};

exports.updateMedia = function (id, newMedia){
  return ddb.updateItem('proposals', id, null, {media:{value:newMedia}}, function (err, proposals) {
      if (err) {
        console.log(err);
      }
  });
};

exports.delete = function (req, res){
  ddb.deleteItem('proposals', req.params.id, null, {},  function (err, proposal) {
      if (!err) {
        console.log("Removed Proposal");
        return res.send('');
      } else {
        console.log(err);
      }
  });
};

exports.findRandom = function (req, res){
  return ddb.scan('proposals', {}, function (err, proposals) {
    if (!err) {
      var items = [];
      for(var i=0;i<req.params.qtd;i++){
        var index = parseInt(Math.random() * proposals.count);
        items.push(proposals.items[index]);
      }
      return res.send(items);
    } else {
      return console.log(err);
    }
  });
};<commit_msg>Alterando forma de se fazer ramdom, primeiro aleatorizando o político depois a proposta<commit_after>'use strict';

var ddb = require('../../config/dynamo_database').ddb;
var _ = require('underscore');

var proposalModel = require('../model/proposalModel');

exports.findAll = function (req, res){
	return ddb.scan('proposals', {}, function (err, proposals) {
		if (!err) {
			return res.send(_.sortBy(proposals.items, function(element){
				return element.tema + element.subtema;
			}) );
		} else {
			return console.log(err);
		}
	});
};

exports.add = function (req, res){
	var proposal = proposalModel.newProposal(req);
	ddb.putItem('proposals', proposal, {}, function (err) {
		if (!err) {
			return console.log("Created Proposal");
		} else {
			return console.log(err);
		}
	});
	return res.send(202,proposal);
};

exports.findById = function (req, res){
	return ddb.getItem('proposals', req.params.id, null, {}, function (err, proposal) {
		if (!err) {
			return res.send(proposal);
		} else {
			return console.log(err);
		}
	});
};

exports.update = function (req, res){
	var proposal = proposalModel.updateProposal(req);

	return ddb.updateItem('proposals', req.params.id, null, proposal, function (err, proposals) {
		if (!err) {
			console.log("Updated Proposal");
		} else {
			console.log(err);
		}
		return res.send(202, proposals);
	});
};

exports.updateMedia = function (id, newMedia){
	return ddb.updateItem('proposals', id, null, {media:{value:newMedia}}, function (err, proposals) {
		if (err) {
			console.log(err);
		}
	});
};

exports.delete = function (req, res){
	ddb.deleteItem('proposals', req.params.id, null, {},  function (err, proposal) {
		if (!err) {
			console.log("Removed Proposal");
			return res.send('');
		} else {
			console.log(err);
		}
	});
};

exports.findRandom = function (req, res){
	return ddb.scan('proposals', {}, function (err, proposals) {
		if (!err) {
			var items = [];
			var politicians = [];
			var politicians_ids = [];

			proposals.items.forEach(function(element,index){
				if(politicians[element.politicians_id] == undefined){
					politicians[element.politicians_id] = [];
					politicians_ids.push(element.politicians_id);
				}
				politicians[element.politicians_id].push(index);
			});
			for(var i=0;i<req.params.qtd;i++){
				var randPolitician = parseInt(Math.random() * politicians_ids.count);
				var index = parseInt(Math.random() * politicians[randPolitician].count);
				items.push(proposals.items[politicians[randPolitician][index]]);
			}
			return res.send(items);
		} else {
			return console.log(err);
		}
	});
};<|endoftext|>


<commit_before>// Module dependencies
'use strict';
var debug = require('debug')('ruche');

/**
 * This function parse a `ruche.json`. Those files describes all the packages
 * available. By passing a `wanted` argument you choose the specifications of
 * the package you want.
 * @summary  Find the best package to install
 * @memberOf RucheUtil
 * @param    {object} wanted Package and spec you want. It's an object with
 *           at least one key `package`. Optionnals keys can be added:
 *           `version` and `platform`.
 * @param    {object} data The content of a `ruche.json` file.
 * @return   {object} Return the info of the best package.
 * @example
 * var rucheUtil = require('./lib/ruche/util');
 * var data = {}; // content of a ruche.json file
 * var match = rucheUtil.match({ package: 'curl' }, data);
 * @requires debug
 * @since    0.0.1
 * @license  [The MIT License (MIT)]{@link http://git.io/QMeU-w}
 */
var match = function match(wanted, data) {

  function calcVersion(item) {
    var calc = '';
    /*jshint unused:false */
    item.split('.').forEach(function (v, i, r) {
      var toFill = 5 - v.length;
      calc += new Array(toFill).join('0') + v;
    });
    return parseInt(calc);
  }

  var dataByVersion = data.sort(function (a, b) {
    return calcVersion(b.version) - calcVersion(a.version);
  });

  var platform;
  if (process.env.PROCESSOR_ARCHITECTURE === 'AMD64') {
    platform = 'win64';
  }
  platform = wanted.platform || platform || 'win32';

  var version = wanted.version || dataByVersion[0].version;
  console.log('search for v%s on %s', version, platform);

  var i;
  var c;
  var tmp = [];
  var len = dataByVersion.length;
  for (i = 0; i < len; i++) {
    c = dataByVersion[i];
    if (c.version === version && c.platform === platform) {
      tmp.push(c);
    }
  }
  if (tmp.length === 0) {
    tmp.push(dataByVersion[0]);
  }
  console.dir(tmp);
  debug('Match %s-%s-%s', c.package, c.version, c.platform);
  return tmp[0];
};

module.exports = match;
<commit_msg>Noting to stdout in ruche<commit_after>// Module dependencies
'use strict';
var debug = require('debug')('ruche');

/**
 * This function parse a `ruche.json`. Those files describes all the packages
 * available. By passing a `wanted` argument you choose the specifications of
 * the package you want.
 * @summary  Find the best package to install
 * @memberOf RucheUtil
 * @param    {object} wanted Package and spec you want. It's an object with
 *           at least one key `package`. Optionnals keys can be added:
 *           `version` and `platform`.
 * @param    {object} data The content of a `ruche.json` file.
 * @return   {object} Return the info of the best package.
 * @example
 * var rucheUtil = require('./lib/ruche/util');
 * var data = {}; // content of a ruche.json file
 * var match = rucheUtil.match({ package: 'curl' }, data);
 * @requires debug
 * @since    0.0.1
 * @license  [The MIT License (MIT)]{@link http://git.io/QMeU-w}
 */
var match = function match(wanted, data) {

  function calcVersion(item) {
    var calc = '';
    /*jshint unused:false */
    item.split('.').forEach(function (v, i, r) {
      var toFill = 5 - v.length;
      calc += new Array(toFill).join('0') + v;
    });
    return parseInt(calc);
  }

  var dataByVersion = data.sort(function (a, b) {
    return calcVersion(b.version) - calcVersion(a.version);
  });

  var platform;
  if (process.env.PROCESSOR_ARCHITECTURE === 'AMD64') {
    platform = 'win64';
  }
  platform = wanted.platform || platform || 'win32';

  var version = wanted.version || dataByVersion[0].version;
  debug('search for v%s on %s', version, platform);

  var i;
  var c;
  var tmp = [];
  var len = dataByVersion.length;
  for (i = 0; i < len; i++) {
    c = dataByVersion[i];
    if (c.version === version && c.platform === platform) {
      tmp.push(c);
    }
  }
  if (tmp.length === 0) {
    tmp.push(dataByVersion[0]);
  }
  debug('Match %s-%s-%s', c.package, c.version, c.platform);
  return tmp[0];
};

module.exports = match;
<|endoftext|>


<commit_before>const Promise = require('bluebird')
const _redis = require('redis')
const crypto = require('crypto')
const event = require('jm-event')
const consts = require('../consts')
const log = require('jm-log4js')
const error = require('jm-err')
const _ = require('lodash')
let Err = consts.Err
let logger = log.getLogger('sso')
Promise.promisifyAll(_redis.RedisClient.prototype)
Promise.promisifyAll(_redis.Multi.prototype)

/**
 * Class representing a tokenMan.
 */
class TokenMan {
  /**
   * Create a tokenMan.
   * @param {Object} opts
   * @example
   * opts参数:{
   *  redis: (可选, 如果不填，自动连接默认 127.0.0.1:6379)
   *  secret: 安全密钥(可选，默认'')
   *  tokenKey: tokenKey, (可选, 默认'sso:token')
   *  tokenExpire: token过期时间, 单位秒(可选, 默认7200秒)
   * }
   */
  constructor (opts = {}) {
    event.enableEvent(this)
    this.secret = opts.secret || ''
    this.tokenKey = opts.tokenKey || consts.TokenKey
    this.IdKey = opts.idKey || consts.IdKey
    this.tokenExpire = opts.tokenExpire || consts.TokenExpire
    this.enableId = opts.enable_id

    let redis = null
    if (opts.redis) {
      redis = _redis.createClient(opts.redis)
    } else {
      redis = _redis.createClient()
    }
    redis.on('ready', () => {
      this.emit('ready')
    })
    redis.on('end', () => {
      this.emit('notready')
    })
    this.redis = redis
  }

  /**
   * 生成token
   * @param {String} id
   * @return {String} token
   */
  create (id = '') {
    id += Math.random() + Date.now().toString() + this.secret
    let sha256 = crypto.createHash('sha256')
    sha256.update(id)
    let token = sha256.digest('hex')
    return token
  }

  getKey (id) {
    return this.tokenKey + ':' + id
  }

  getIdKey (id) {
    return this.IdKey + ':' + id
  }

  async getValueById (id) {
    try {
      let mapToken = await this.redis.getAsync(this.getIdKey(id))
      mapToken || (mapToken = '{}')
      mapToken = JSON.parse(mapToken)
      for (let token in mapToken) {
        let o = mapToken[token]
        let expired = o.time + o.expire * 1000
        if (expired <= Date.now()) delete mapToken[token]
      }
      return mapToken
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_INVALID_ID)
    }
  }

  /**
   * 添加用户token, 并返回登记信息
   * @param {Object} opts
   * @example
   * opts参数:{
   *  id: user id(可选)
   *  token: token(可选)
   *  expire: token过期时间, 单位秒, 0代表永不过期(可选)
   *  data: 用户数据(可选)
   * }
   * @returns {Promise<*>}
   */
  async add (opts = {}) {
    let redis = this.redis
    opts.expire || (opts.expire = this.tokenExpire)
    opts.token || (opts.token = this.create(opts.id))
    opts.time || (opts.time = Date.now())
    try {
      await redis.setAsync(this.getKey(opts.token), JSON.stringify(opts), 'EX', opts.expire)
      if (this.enableId && opts.id) {
        let mapToken = await this.getValueById(opts.id)
        mapToken[opts.token] = opts
        await redis.setAsync(this.getIdKey(opts.id), JSON.stringify(mapToken), 'EX', opts.expire)
      }
      return opts
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_ADD_TOKEN)
    }
  }

  /**
   * 验证用户token, 如果成功, 返回登记信息
   * @param token
   * @returns {Promise<*>}
   */
  async verify (token) {
    if (!token) throw error.err(Err.FA_INVALID_TOKEN)
    try {
      let doc = await this.redis.getAsync(this.getKey(token))
      if (!doc) throw error.err(Err.FA_INVALID_TOKEN)
      doc = JSON.parse(doc)
      return doc
    } catch (e) {
      if (e.code) throw e
      logger.error(e)
      throw error.err(Err.FA_VERIFY_TOKEN)
    }
  }

  /**
   * 延长用户token过期时间, 并返回登记信息
   * @param token
   * @param opts (可选)
   * @example
   * opts参数:{
   *  expire: token过期时间, 单位秒(可选)
   *  data: 用户数据(可选)
   * }
   * @returns {Promise<*>}
   */
  async touch (token, opts = {}) {
    let doc = await this.verify(token)
    _.assign(doc, opts)
    doc.expire || (doc.expire = this.tokenExpire)
    doc.time = Date.now()
    try {
      await this.redis.setAsync(this.getKey(token), JSON.stringify(doc), 'EX', doc.expire)
      if (this.enableId && doc.id) {
        await this.redis.expireAsync(this.getIdKey(doc.id), doc.expire)
      }
      return doc
    } catch (e) {
      logger.error(e)
      if (!token) throw error.err(Err.FA_TOUCH_TOKEN)
    }
  }

  /**
   * 删除用户token, 并返回token
   * @param token
   * @returns {Promise<*>}
   */
  async delete (token) {
    if (!token) throw error.err(Err.FA_INVALID_TOKEN)
    try {
      if (this.enableId) {
        let doc = await this.redis.getAsync(this.getKey(token))
        if (doc) {
          doc = JSON.parse(doc)
          if (doc.id) {
            let mapToken = await this.getValueById(doc.id)
            delete mapToken[token]
            await this.redis.setAsync(this.getIdKey(doc.id), JSON.stringify(mapToken))
          }
        }
      }
      await this.redis.delAsync(this.getKey(token))
      return token
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_DELETE_TOKEN)
    }
  }

  async deleteById (id) {
    if (!id) throw error.err(Err.FA_INVALID_ID)
    try {
      let mapToken = await this.getValueById(id)
      let tokens = Object.keys(mapToken)
      let promises = tokens.map((token) => this.delete(token))
      let results = await Promise.all(promises)
      await this.redis.delAsync(this.getIdKey(id))
      return results
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_DELETE_TOKEN)
    }
  }
}

module.exports = TokenMan
<commit_msg>过期时间比较取最长的<commit_after>const Promise = require('bluebird')
const _redis = require('redis')
const crypto = require('crypto')
const event = require('jm-event')
const consts = require('../consts')
const log = require('jm-log4js')
const error = require('jm-err')
const _ = require('lodash')
let Err = consts.Err
let logger = log.getLogger('sso')
Promise.promisifyAll(_redis.RedisClient.prototype)
Promise.promisifyAll(_redis.Multi.prototype)

/**
 * Class representing a tokenMan.
 */
class TokenMan {
  /**
   * Create a tokenMan.
   * @param {Object} opts
   * @example
   * opts参数:{
   *  redis: (可选, 如果不填，自动连接默认 127.0.0.1:6379)
   *  secret: 安全密钥(可选，默认'')
   *  tokenKey: tokenKey, (可选, 默认'sso:token')
   *  tokenExpire: token过期时间, 单位秒(可选, 默认7200秒)
   * }
   */
  constructor (opts = {}) {
    event.enableEvent(this)
    this.secret = opts.secret || ''
    this.tokenKey = opts.tokenKey || consts.TokenKey
    this.IdKey = opts.idKey || consts.IdKey
    this.tokenExpire = opts.tokenExpire || consts.TokenExpire
    this.enableId = opts.enable_id

    let redis = null
    if (opts.redis) {
      redis = _redis.createClient(opts.redis)
    } else {
      redis = _redis.createClient()
    }
    redis.on('ready', () => {
      this.emit('ready')
    })
    redis.on('end', () => {
      this.emit('notready')
    })
    this.redis = redis
  }

  /**
   * 生成token
   * @param {String} id
   * @return {String} token
   */
  create (id = '') {
    id += Math.random() + Date.now().toString() + this.secret
    let sha256 = crypto.createHash('sha256')
    sha256.update(id)
    let token = sha256.digest('hex')
    return token
  }

  getKey (id) {
    return this.tokenKey + ':' + id
  }

  getIdKey (id) {
    return this.IdKey + ':' + id
  }

  async getValueById (id) {
    try {
      let mapToken = await this.redis.getAsync(this.getIdKey(id))
      mapToken || (mapToken = '{}')
      mapToken = JSON.parse(mapToken)
      for (let token in mapToken) {
        let o = mapToken[token]
        let expired = o.time + o.expire * 1000
        if (expired <= Date.now()) delete mapToken[token]
      }
      return mapToken
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_INVALID_ID)
    }
  }

  /**
   * 添加用户token, 并返回登记信息
   * @param {Object} opts
   * @example
   * opts参数:{
   *  id: user id(可选)
   *  token: token(可选)
   *  expire: token过期时间, 单位秒, 0代表永不过期(可选)
   *  data: 用户数据(可选)
   * }
   * @returns {Promise<*>}
   */
  async add (opts = {}) {
    let redis = this.redis
    opts.expire || (opts.expire = this.tokenExpire)
    opts.token || (opts.token = this.create(opts.id))
    opts.time || (opts.time = Date.now())
    try {
      await redis.setAsync(this.getKey(opts.token), JSON.stringify(opts), 'EX', opts.expire)
      if (this.enableId && opts.id) {
        let mapToken = await this.getValueById(opts.id)
        mapToken[opts.token] = opts
        let expire = await redis.ttlAsync(this.getIdKey(opts.id))
        if (expire < Number(opts.expire)) expire = opts.expire
        await redis.setAsync(this.getIdKey(opts.id), JSON.stringify(mapToken), 'EX', expire)
      }
      return opts
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_ADD_TOKEN)
    }
  }

  /**
   * 验证用户token, 如果成功, 返回登记信息
   * @param token
   * @returns {Promise<*>}
   */
  async verify (token) {
    if (!token) throw error.err(Err.FA_INVALID_TOKEN)
    try {
      let doc = await this.redis.getAsync(this.getKey(token))
      if (!doc) throw error.err(Err.FA_INVALID_TOKEN)
      doc = JSON.parse(doc)
      return doc
    } catch (e) {
      if (e.code) throw e
      logger.error(e)
      throw error.err(Err.FA_VERIFY_TOKEN)
    }
  }

  /**
   * 延长用户token过期时间, 并返回登记信息
   * @param token
   * @param opts (可选)
   * @example
   * opts参数:{
   *  expire: token过期时间, 单位秒(可选)
   *  data: 用户数据(可选)
   * }
   * @returns {Promise<*>}
   */
  async touch (token, opts = {}) {
    let doc = await this.verify(token)
    _.assign(doc, opts)
    doc.expire || (doc.expire = this.tokenExpire)
    doc.time = Date.now()
    try {
      await this.redis.setAsync(this.getKey(token), JSON.stringify(doc), 'EX', doc.expire)
      if (this.enableId && doc.id) {
        let expire = await this.redis.ttlAsync(this.getIdKey(doc.id))
        if (expire < Number(doc.expire)) expire = doc.expire
        await this.redis.expireAsync(this.getIdKey(doc.id), expire)
      }
      return doc
    } catch (e) {
      logger.error(e)
      if (!token) throw error.err(Err.FA_TOUCH_TOKEN)
    }
  }

  /**
   * 删除用户token, 并返回token
   * @param token
   * @returns {Promise<*>}
   */
  async delete (token) {
    if (!token) throw error.err(Err.FA_INVALID_TOKEN)
    try {
      if (this.enableId) {
        let doc = await this.redis.getAsync(this.getKey(token))
        if (doc) {
          doc = JSON.parse(doc)
          if (doc.id) {
            let mapToken = await this.getValueById(doc.id)
            delete mapToken[token]
            await this.redis.setAsync(this.getIdKey(doc.id), JSON.stringify(mapToken))
          }
        }
      }
      await this.redis.delAsync(this.getKey(token))
      return token
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_DELETE_TOKEN)
    }
  }

  async deleteById (id) {
    if (!id) throw error.err(Err.FA_INVALID_ID)
    try {
      let mapToken = await this.getValueById(id)
      let tokens = Object.keys(mapToken)
      let promises = tokens.map((token) => this.delete(token))
      let results = await Promise.all(promises)
      await this.redis.delAsync(this.getIdKey(id))
      return results
    } catch (e) {
      logger.error(e)
      throw error.err(Err.FA_DELETE_TOKEN)
    }
  }
}

module.exports = TokenMan
<|endoftext|>