<commit_before>/**
 * HEY, DON'T EDIT THIS FILE!
 * It is assembled by a grunt script. You should be edit the files in the src
 * folder and then run grunt in the nodebotui root
 **/

var nodebotui = (function () {

  var socket, boards;

  /**
   * Board
   * @constructor
   *
   * @param {Object} opts
   */
  var Board = function( opts ) {

    if ( !(this instanceof Board) ) {
      return new Board( opts );
    }

    this._element = opts.element;

    this.getInputs();

  }

  /**
   * Method to loop through all the inputs within a form, if they
   * have a data-device-type defined then initialize them and
   * associate them with the board
   */
  Board.prototype.getInputs = function() {

    var inputs = document.getElementById(this._element).getElementsByTagName('input');
    for (i = 0; i < inputs.length; i++) {
      if (inputs[i].hasAttribute('data-device-type')) {
        this[inputs[i].id] = new Input({'element': inputs[i], 'board': this._element });
      }
    }

    var fieldsets = document.getElementById(this._element).getElementsByTagName('fieldset');
    for (i = 0; i < fieldsets.length; i++) {
      if (fieldsets[i].hasAttribute('data-device-type')) {
        this['_'+fieldsets[i].id] = new BrowserControl({'element': fieldsets[i], 'board': this._element });
      }
    }

  }

  /**
   * Method to initialize all the input devices and make sure
   * that their initial values are set on the server side
   */
  Board.prototype.initialize = function() {
    _each(this, function(device, key) {
      if (key.substring(0,1) !== '_' && device._initialize) {
        device._initialize();
      }
    });
  }

  /**
   * Input
   * @constructor
   *
   * @param {Object} opts
   */
  var Input = function( opts ) {

    if ( !(this instanceof Input) ) {
      return new Input( opts );
    }

    // Cache the element and set common attriubutes
    var el = document.getElementById(opts.element.id);
    this._board = opts.board;
    this._element = el.id;
    this.pin = el.getAttribute('data-pin');

    // Extend this object with the devicetype properties and methods
    this.dataDeviceType = el.getAttribute('data-device-type');
    _extend(this, deviceTypes[this.dataDeviceType]);

    // Extend this object with inputType properties and methods
    this.inputType = el.getAttribute('type');
    _extend(this, inputTypes[this.inputType]);

    // Bind event listeners
    this._listen(el, this);

    // Add required methods to the object
    _each(this._methods, function(method, index) {
      this[method] = deviceMethods[method];
    }, this);

    delete this._methods;

  }

  /**
   * Browser Control
   * @constructor
   *
   * @param {Object} opts
   */
  var BrowserControl = function( opts ) {

    if ( !(this instanceof BrowserControl) ) {
      return new BrowserControl( opts );
    }

    // Cache the element and set common attributes
    var el = document.getElementById(opts.element.id);
    this._board = opts.board;
    this._element = el.id;

    // Extend this object with the browser control properties and methods
    this.dataDeviceType = el.getAttribute('data-device-type');
    _extend(this, browserControls[this.dataDeviceType]);

    // Initialize object
    this._initialize(el, this);

    // Bind event listeners
    this._listen(el, this);

  }

  /**
   * These are all the device types defined in Johnny-Five. These
   * extend our Input objects (not elements)
   *
   * min - Minimum value for the device type
   * max - Maximum value for the device type
   * initial - Initial value for the device type
   * _methods - A list of deviceMethods names that apply to the device type
   */
  var deviceTypes = {
    Led: {
      min: 0,
      max: 255,
      _methods: ['on', 'off'] //, 'toggle', 'brightness', 'pulse', 'fade', 'fadeIn', 'fadeOut', 'strobe', 'stop']
    },
    Servo: {
      min: 0,
      max: 180,
      initial: 90,
      _methods: ['move'] //, 'center', 'sweep'
    }
  }

  /**
   * All the methods that are defined in Johnny-Five. These extend
   * our Input objects (not elements)
   */
   var deviceMethods = {
    on: function() {
      if (socket) {
        socket.emit('call', { "board": this._board, "device": this._element, "method": "on" });
      }
      this._update(true);
    },
    off: function() {
      if (socket) {
        socket.emit('call', { "board": this._board, "device": this._element, "method": "off" });
      }
      this._update(false);
    },
    move: function(value) {
      if (value === null) {
        value = Number(document.getElementById(this._element).value)
      }
      if (socket) {
        socket.emit('call', { "board": this._board, "device": this._element, "method": "move", params: value });
      }
      this._update( value );
    }
  }

  /**
   * These are browser controls.
   * Browser controls are groups of inputs working in concert
   *
   * _listen - A function that binds necessary event listeners to the <input> elements
   */
  var browserControls = {

    /**
     * fieldset data-device-type="orientation"
     *
     * A group of two or three ranges
     **/
    Orientation: {

      /**
       * On deviceorientation check to see if there are inputs for each of the three axes
       * If so, move that range input
       **/
      _listen: function(el, browserControl) {
        window.addEventListener('deviceorientation', function(event) {
          _each(['alpha', 'beta', 'gamma'], function (prefix) {
            if (browserControl[prefix+'Input']) {
              boards[browserControl._board][browserControl[prefix+'Input']].move((event[prefix]+180)/2);
            }
          }, this);
        });
      },

      _update: function(alpha, beta, gamma) {
        //todo
      },

      /**
       * Bind each of the inputs associated with the browser control with
       * the appropriate axis
       **/
       _initialize: function(el, browserControl) {
        var inputs = document.getElementById(this._element).getElementsByTagName('input');
        for (i = 0; i < inputs.length; i++) {
          if (inputs[i].hasAttribute('data-axis')) {
            _each(['alpha', 'beta', 'gamma'], function (prefix) {
              if (inputs[i].getAttribute('data-axis') === prefix) {
                this[prefix+'Input'] = inputs[i].id;
              }
            }, this);
          }
        }

      }
    }
  }
  /**
   * These are all the HTML input types we recognize.
   * Inputs can be standalone or grouped to form a browser control.
   *
   * _listen - A function that binds necessary event listeners to the <input> elements
   */
  var inputTypes = {

    /**
     * input type="checkbox"
     *
     * Can be bound to an LED
     **/
    checkbox: {
      _listen: function(el, input) {
        el.addEventListener('change', function() {
          el.checked ? input.on() : input.off();
        });
      },
      _update: function(status) {
        document.getElementById(this._element).checked = status;
      }
    },

    /**
     * input type="range"
     *
     * Can be bound to a servo
     **/
     range: {
      _listen: function(el, input) {
        el.addEventListener('change', function() {
          input.move()
        });
      },
      _update: function(status) {
        document.getElementById(this._element).value = status;
      },
      _initialize: function() {
        var el = document.getElementById(this._element);

        this.max = el.getAttribute('max') || this.max;
        el.setAttribute('max', this.max);

        this.min = el.getAttribute('min') || this.min;
        el.setAttribute('min', this.min);
      }
    }
  }

  /**
   * These are browser controls.
   * Browser controls are groups of inputs working in concert
   *
   * _listen - A function that binds necessary event listeners to the <input> elements
   */
  var browserControls = {

    /**
     * fieldset data-device-type="orientation"
     *
     * A group of two or three ranges
     **/
    Orientation: {

      /**
       * On deviceorientation check to see if there are inputs for each of the three axes
       * If so, move that range input
       **/
      _listen: function(el, browserControl) {
        window.addEventListener('deviceorientation', function(event) {
          _each(['alpha', 'beta', 'gamma'], function (prefix) {
            if (browserControl[prefix+'Input']) {
              boards[browserControl._board][browserControl[prefix+'Input']].move((event[prefix]+180)/2);
            }
          }, this);
        });
      },

      _update: function(alpha, beta, gamma) {
        //todo
      },

      /**
       * Bind each of the inputs associated with the browser control with
       * the appropriate axis
       **/
       _initialize: function(el, browserControl) {
        var inputs = document.getElementById(this._element).getElementsByTagName('input');
        for (i = 0; i < inputs.length; i++) {
          if (inputs[i].hasAttribute('data-axis')) {
            _each(['alpha', 'beta', 'gamma'], function (prefix) {
              if (inputs[i].getAttribute('data-axis') === prefix) {
                this[prefix+'Input'] = inputs[i].id;
              }
            }, this);
          }
        }

      }
    }
  }
  /**
   * The following is, ahem, borrowed code
   */

  // extend function from underscore.js http://underscorejs.org/#extend
  function _extend(obj) {
    _each(Array.prototype.slice.call(arguments, 1), function(source) {
      if (source) {
        for (var prop in source) {
          obj[prop] = source[prop];
        }
      }
    });
    return obj;
  };

  // each function from underscore.js http://underscorejs.org/#each
  function _each(obj, iterator, context) {
    if (obj == null) return;
    if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, length = obj.length; i < length; i++) {
        if (iterator.call(context, obj[i], i, obj) === {}) return;
      }
    } else {
      var keys = _keys(obj);
      for (var i = 0, length = keys.length; i < length; i++) {
        if (iterator.call(context, obj[keys[i]], keys[i], obj) === {}) return;
      }
    }
  };

  // keys function from underscore.js http://underscorejs.org/#keys
  function _keys(obj) {
    if (obj !== Object(obj)) throw new TypeError('Invalid object');
    var keys = [];
    for (var key in obj) if (hasOwnProperty.call(obj, key)) keys.push(key);
    return keys;
  };

  /**
   * Loop through the forms in the web page. For each one that has a
   * data-device-type attribute set to "board" we are defining a
   * a new board to pass to Johnny-Five
   */
  function _getBoards() {

    var boards = {}, forms = document.getElementsByTagName ('form'), i;

    for (i = 0; i < forms.length; i++) {
      if (forms[i].getAttribute('data-device-type') === 'board') {
        boards[forms[i].id] = new Board({ 'element': forms[i].id});

      }
    }

    return boards;
  }

  /**
   * This bit loads the socket.io client script asynchronously
   * and then fires our _getBoards function
   **/
  var script = document.createElement('script');
  script.type = 'text/javascript';
  script.async = true;

  /**
   * Find the src for this script so we can request socket.io
   * from the same server
   **/
  var scripts = document.getElementsByTagName('script'), len = scripts.length, re = /nodebotui-client\.js$/, src, nbuiScriptSrc;
  while (len--) {
    src = scripts[len].src;
    if (src && src.match(re)) {
      nbuiScriptSrc = src;
      break;
    }
  }

  script.onload = function(){

      socket = io.connect(nbuiScriptSrc.replace('/nodebotui/nodebotui-client.js', ''));

      // tell the server to initialize our new boards
      _each(boards, function( board, key) {
        socket.emit('new board', board );
      });

      // This is where we listen for events from the server
      socket.on('board ready', function( opts ) {
        boards[opts.id]._ready = true;
        boards[opts.id].initialize();
        console.log('board ready');
      });

  };

  // Insert script element for socket.io
  script.src = nbuiScriptSrc.replace('nodebotui/nodebotui-client.js', 'socket.io/socket.io.js');
  document.getElementsByTagName('head')[0].appendChild(script);

  //Initialize boards
  boards =  _getBoards();

  // assign our boards object to nodebotui global
  return boards;

})();<commit_msg>New build<commit_after>/**
 * HEY, DON'T EDIT THIS FILE!
 * It is assembled by a grunt script. You should be edit the files in the src
 * folder and then run grunt in the nodebotui root
 **/

var nodebotui = (function () {

  var socket, boards;

  /**
   * Board
   * @constructor
   *
   * @param {Object} opts
   */
  var Board = function( opts ) {

    if ( !(this instanceof Board) ) {
      return new Board( opts );
    }

    this._element = opts.element;

    this.getInputs();

  }

  /**
   * Method to loop through all the inputs within a form, if they
   * have a data-device-type defined then initialize them and
   * associate them with the board
   */
  Board.prototype.getInputs = function() {

    var inputs = document.getElementById(this._element).getElementsByTagName('input');
    for (i = 0; i < inputs.length; i++) {
      if (inputs[i].hasAttribute('data-device-type')) {
        this[inputs[i].id] = new Input({'element': inputs[i], 'board': this._element });
      }
    }

    var fieldsets = document.getElementById(this._element).getElementsByTagName('fieldset');
    for (i = 0; i < fieldsets.length; i++) {
      if (fieldsets[i].hasAttribute('data-device-type')) {
        this['_'+fieldsets[i].id] = new BrowserControl({'element': fieldsets[i], 'board': this._element });
      }
    }

  }

  /**
   * Method to initialize all the input devices and make sure
   * that their initial values are set on the server side
   */
  Board.prototype.initialize = function() {
    _each(this, function(device, key) {
      if (key.substring(0,1) !== '_' && device._initialize) {
        device._initialize();
      }
    });
  }

  /**
   * Input
   * @constructor
   *
   * @param {Object} opts
   */
  var Input = function( opts ) {

    if ( !(this instanceof Input) ) {
      return new Input( opts );
    }

    // Cache the element and set common attriubutes
    var el = document.getElementById(opts.element.id);
    this._board = opts.board;
    this._element = el.id;
    this.pin = el.getAttribute('data-pin');

    // Extend this object with the devicetype properties and methods
    this.dataDeviceType = el.getAttribute('data-device-type');
    _extend(this, deviceTypes[this.dataDeviceType]);

    // Extend this object with inputType properties and methods
    this.inputType = el.getAttribute('type');
    _extend(this, inputTypes[this.inputType]);

    // Bind event listeners
    this._listen(el, this);

    // Add required methods to the object
    _each(this._methods, function(method, index) {
      this[method] = deviceMethods[method];
    }, this);

    delete this._methods;

  }

  /**
   * Browser Control
   * @constructor
   *
   * @param {Object} opts
   */
  var BrowserControl = function( opts ) {

    if ( !(this instanceof BrowserControl) ) {
      return new BrowserControl( opts );
    }

    // Cache the element and set common attributes
    var el = document.getElementById(opts.element.id);
    this._board = opts.board;
    this._element = el.id;

    // Extend this object with the browser control properties and methods
    this.dataDeviceType = el.getAttribute('data-device-type');
    _extend(this, browserControls[this.dataDeviceType]);

    // Initialize object
    this._initialize(el, this);

    // Bind event listeners
    this._listen(el, this);

  }

  /**
   * These are all the device types defined in Johnny-Five. These
   * extend our Input objects (not elements)
   *
   * min - Minimum value for the device type
   * max - Maximum value for the device type
   * initial - Initial value for the device type
   * _methods - A list of deviceMethods names that apply to the device type
   */
  var deviceTypes = {
    Led: {
      min: 0,
      max: 255,
      _methods: ['on', 'off'] //, 'toggle', 'brightness', 'pulse', 'fade', 'fadeIn', 'fadeOut', 'strobe', 'stop']
    },
    Servo: {
      min: 0,
      max: 180,
      initial: 90,
      _methods: ['move'] //, 'center', 'sweep'
    }
  }

  /**
   * All the methods that are defined in Johnny-Five. These extend
   * our Input objects (not elements)
   */
   var deviceMethods = {
    on: function() {
      if (socket) {
        socket.emit('call', { "board": this._board, "device": this._element, "method": "on" });
      }
      this._update(true);
    },
    off: function() {
      if (socket) {
        socket.emit('call', { "board": this._board, "device": this._element, "method": "off" });
      }
      this._update(false);
    },
    move: function(value) {
      if (value === null) {
        value = Number(document.getElementById(this._element).value)
      }
      if (socket) {
        socket.emit('call', { "board": this._board, "device": this._element, "method": "move", params: value });
      }
      this._update( value );
    }
  }

  /**
   * These are browser controls.
   * Browser controls are groups of inputs working in concert
   *
   * _listen - A function that binds necessary event listeners to the <input> elements
   */
  var browserControls = {

    /**
     * fieldset data-device-type="orientation"
     *
     * A group of two or three ranges
     **/
    Orientation: {
       /**
       * On deviceorientation check to see if there are inputs for each of the three axes
       * If so, move that range input
       **/
      _listen: function(el, browserControl) {
        window.addEventListener('deviceorientation', function(event) {
          _each(['alpha', 'beta', 'gamma'], function (prefix) {
            if (browserControl[prefix+'Input']) {
              var value = (event[prefix] + 90);
              var thisInput = boards[browserControl._board][browserControl[prefix+'Input']];
              if (prefix === 'beta') console.log(event[prefix], value);
              boards[browserControl._board][browserControl[prefix+'Input']].move(value);
            }
          }, this);
        });
      },

      _update: function(alpha, beta, gamma) {
        //todo
      },

      /**
       * Bind each of the inputs associated with the browser control with
       * the appropriate axis
       **/
       _initialize: function(el, browserControl) {
        var inputs = document.getElementById(this._element).getElementsByTagName('input');
        for (i = 0; i < inputs.length; i++) {
          if (inputs[i].hasAttribute('data-axis')) {
            _each(['alpha', 'beta', 'gamma'], function (prefix) {
              if (inputs[i].getAttribute('data-axis') === prefix) {
                this[prefix+'Input'] = inputs[i].id;
              }
            }, this);
          }
        }

      }
    }
  }
  /**
   * These are all the HTML input types we recognize.
   * Inputs can be standalone or grouped to form a browser control.
   *
   * _listen - A function that binds necessary event listeners to the <input> elements
   */
  var inputTypes = {

    /**
     * input type="checkbox"
     *
     * Can be bound to an LED
     **/
    checkbox: {
      _listen: function(el, input) {
        el.addEventListener('change', function() {
          el.checked ? input.on() : input.off();
        });
      },
      _update: function(status) {
        document.getElementById(this._element).checked = status;
      }
    },

    /**
     * input type="range"
     *
     * Can be bound to a servo
     **/
     range: {
      _listen: function(el, input) {
        el.addEventListener('change', function() {
          input.move()
        });
      },
      _update: function(status) {
        document.getElementById(this._element).value = status;
      },
      _initialize: function() {
        var el = document.getElementById(this._element);

        this.max = el.getAttribute('max') || this.max;
        el.setAttribute('max', this.max);

        this.min = el.getAttribute('min') || this.min;
        el.setAttribute('min', this.min);
      }
    }
  }

  /**
   * These are browser controls.
   * Browser controls are groups of inputs working in concert
   *
   * _listen - A function that binds necessary event listeners to the <input> elements
   */
  var browserControls = {

    /**
     * fieldset data-device-type="orientation"
     *
     * A group of two or three ranges
     **/
    Orientation: {
       /**
       * On deviceorientation check to see if there are inputs for each of the three axes
       * If so, move that range input
       **/
      _listen: function(el, browserControl) {
        window.addEventListener('deviceorientation', function(event) {
          _each(['alpha', 'beta', 'gamma'], function (prefix) {
            if (browserControl[prefix+'Input']) {
              var value = (event[prefix] + 90);
              var thisInput = boards[browserControl._board][browserControl[prefix+'Input']];
              if (prefix === 'beta') console.log(event[prefix], value);
              boards[browserControl._board][browserControl[prefix+'Input']].move(value);
            }
          }, this);
        });
      },

      _update: function(alpha, beta, gamma) {
        //todo
      },

      /**
       * Bind each of the inputs associated with the browser control with
       * the appropriate axis
       **/
       _initialize: function(el, browserControl) {
        var inputs = document.getElementById(this._element).getElementsByTagName('input');
        for (i = 0; i < inputs.length; i++) {
          if (inputs[i].hasAttribute('data-axis')) {
            _each(['alpha', 'beta', 'gamma'], function (prefix) {
              if (inputs[i].getAttribute('data-axis') === prefix) {
                this[prefix+'Input'] = inputs[i].id;
              }
            }, this);
          }
        }

      }
    }
  }
  /**
   * The following is, ahem, borrowed code
   */

  // extend function from underscore.js http://underscorejs.org/#extend
  function _extend(obj) {
    _each(Array.prototype.slice.call(arguments, 1), function(source) {
      if (source) {
        for (var prop in source) {
          obj[prop] = source[prop];
        }
      }
    });
    return obj;
  };

  // each function from underscore.js http://underscorejs.org/#each
  function _each(obj, iterator, context) {
    if (obj == null) return;
    if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, length = obj.length; i < length; i++) {
        if (iterator.call(context, obj[i], i, obj) === {}) return;
      }
    } else {
      var keys = _keys(obj);
      for (var i = 0, length = keys.length; i < length; i++) {
        if (iterator.call(context, obj[keys[i]], keys[i], obj) === {}) return;
      }
    }
  };

  // keys function from underscore.js http://underscorejs.org/#keys
  function _keys(obj) {
    if (obj !== Object(obj)) throw new TypeError('Invalid object');
    var keys = [];
    for (var key in obj) if (hasOwnProperty.call(obj, key)) keys.push(key);
    return keys;
  };

  /**
   * Loop through the forms in the web page. For each one that has a
   * data-device-type attribute set to "board" we are defining a
   * a new board to pass to Johnny-Five
   */
  function _getBoards() {

    var boards = {}, forms = document.getElementsByTagName ('form'), i;

    for (i = 0; i < forms.length; i++) {
      if (forms[i].getAttribute('data-device-type') === 'board') {
        boards[forms[i].id] = new Board({ 'element': forms[i].id});

      }
    }

    return boards;
  }

  /**
   * This bit loads the socket.io client script asynchronously
   * and then fires our _getBoards function
   **/
  var script = document.createElement('script');
  script.type = 'text/javascript';
  script.async = true;

  /**
   * Find the src for this script so we can request socket.io
   * from the same server
   **/
  var scripts = document.getElementsByTagName('script'), len = scripts.length, re = /nodebotui-client\.js$/, src, nbuiScriptSrc;
  while (len--) {
    src = scripts[len].src;
    if (src && src.match(re)) {
      nbuiScriptSrc = src;
      break;
    }
  }

  script.onload = function(){

      socket = io.connect(nbuiScriptSrc.replace('/nodebotui/nodebotui-client.js', ''));

      // tell the server to initialize our new boards
      _each(boards, function( board, key) {
        socket.emit('new board', board );
      });

      // This is where we listen for events from the server
      socket.on('board ready', function( opts ) {
        boards[opts.id]._ready = true;
        boards[opts.id].initialize();
        console.log('board ready');
      });

  };

  // Insert script element for socket.io
  script.src = nbuiScriptSrc.replace('nodebotui/nodebotui-client.js', 'socket.io/socket.io.js');
  document.getElementsByTagName('head')[0].appendChild(script);

  //Initialize boards
  boards =  _getBoards();

  // assign our boards object to nodebotui global
  return boards;

})();<|endoftext|>


<commit_before>angular.module('blueRidgeApp')
.controller('ProjectsCtrl',function($scope,$location,$filter,Restangular,Auth){
    if (!Auth.isSignedIn()) {
        $location.path('/');
    }

    $scope.noob=Auth.isNoob();
    $scope.shouldBeOpen = false;
    $scope.loading = true;
    blueRidgeUser= Restangular.one('users',Auth.currentUser());

    blueRidgeUser.getList('projects').then(function(response){
        $scope.projects = response.projects;
        $scope.loading = false;

    });

    $scope.subscription={
        plan:{id:'',name:''},
        card:''
    }
    $scope.maxalert=false;
    $scope.max=1;
    blueRidgeUser.one('subscription').get().then(function(subscription){
        $scope.subscription=subscription;
        switch(subscription.plan.id){
            case 'br-solo':
            max = 5;
            message= "The Solo plan allows you to see 5 projects at a time. If you'd like to see more (and who wouldn't), please upgrade to Pro.";
            break;
            case 'br-pro':
            message= "Wow! You've maxed out the Pro plan. If you really need to see more than 20 projects at once please let us know. Maybe we'll make a special plan for you. &#x263A";
            max = 20;
            default:
            max = 1;
            message= "Slow down, sparky. You can only see one project at a time on the Free plan. Please upgrade to see more than one at a time.";
        };
        $scope.max=max;
        $scope.maxmessage=message;

    });

    $scope.isMaxed= function isMaxed(){
        if($scope.selectedCount == max){
            return true;
        }
        return false;
    };

    $scope.checkChange = function checkChange (project) {
        if (project.selected) {
            $scope.currentProject = project;
        }
        $scope.selectedCount = 0;
        angular.forEach($scope.projects, function(p) {
            $scope.selectedCount += (p.selected ? 1 : 0);
        });

    }

    $scope.update= function update() {
        var projects = $scope.projects;
        var selected=[];

        angular.forEach(projects,function(project){
            if(project.selected){
                selected.push(project.id);
            }
        });

        blueRidgeUser.profile = {
            projects:selected
        };
        blueRidgeUser.put().then(function(){
            $scope.updated = true;
            $location.path('/app/todos');
            Auth.promoteNoob();
        });
    };
});<commit_msg>-n  grunt fix<commit_after>angular.module('blueRidgeApp')
.controller('ProjectsCtrl',function($scope,$location,$filter,Restangular,Auth){
    if (!Auth.isSignedIn()) {
        $location.path('/');
    }

    $scope.noob=Auth.isNoob();
    $scope.shouldBeOpen = false;
    $scope.loading = true;
    blueRidgeUser= Restangular.one('users',Auth.currentUser());

    blueRidgeUser.getList('projects').then(function(response){
        $scope.projects = response.projects;
        $scope.loading = false;

    });

    $scope.subscription={
        plan:{id:'',name:''},
        card:''
    };
    $scope.maxalert=false;
    $scope.max=1;
    blueRidgeUser.one('subscription').get().then(function(subscription){
        $scope.subscription=subscription;
        switch(subscription.plan.id){
            case 'br-solo':
            max = 5;
            message= "The Solo plan allows you to see 5 projects at a time. If you'd like to see more (and who wouldn't), please upgrade to Pro.";
            break;
            case 'br-pro':
            message= "Wow! You've maxed out the Pro plan. If you really need to see more than 20 projects at once please let us know. Maybe we'll make a special plan for you. &#x263A";
            max = 20;
            break;
            default:
            max = 1;
            message= "Slow down, sparky. You can only see one project at a time on the Free plan. Please upgrade to see more than one at a time.";
        }
        $scope.max=max;
        $scope.maxmessage=message;

    });

    $scope.isMaxed= function isMaxed(){
        if($scope.selectedCount == max){
            return true;
        }
        return false;
    };

    $scope.checkChange = function checkChange (project) {
        if (project.selected) {
            $scope.currentProject = project;
        }
        $scope.selectedCount = 0;
        angular.forEach($scope.projects, function(p) {
            $scope.selectedCount += (p.selected ? 1 : 0);
        });

    };

    $scope.update= function update() {
        var projects = $scope.projects;
        var selected=[];

        angular.forEach(projects,function(project){
            if(project.selected){
                selected.push(project.id);
            }
        });

        blueRidgeUser.profile = {
            projects:selected
        };
        blueRidgeUser.put().then(function(){
            $scope.updated = true;
            $location.path('/app/todos');
            Auth.promoteNoob();
        });
    };
});<|endoftext|>


<commit_before>import React from "react";
import { Link, Route } from "react-router-dom";
import debounce from 'lodash.debounce'

import SearchBook from "./SearchBook";
import BookShelf from "./BookShelf";
import SearchResults from "./SearchResults"
import Page404 from "./Page404"

import * as BooksAPI from "./BooksAPI";
import "./App.css";

class App extends React.Component {
	state = {
		books: [],
		bookResults: []
	};

	componentDidMount() {
		BooksAPI.getAll().then(books => {
			this.setState({ books });
		});
	}

	setBooksAgain() {
		BooksAPI.getAll().then(books => {
			this.setState({ books });
		});
	}

	changeBookShelf = (book_to_change, event) => {
		BooksAPI.update(book_to_change, event.target.value).then(result => {
			this.setBooksAgain();
		});
	};

	updateBook = (book, shelf) => {
		BooksAPI.update(book, shelf).then(res => {
			this.setBooksAgain()
		})
	}

	resetSearchResultState = () => {
		this.setState({ bookResults: [] });
	}

	convertToCamelCase = (str) => {
		var out = "";
		str.split(" ").forEach(function (el, idx) {
			var add = el.toLowerCase();
			out += (idx === 0 ? add : add[0].toUpperCase() + add.slice(1));
		});
		return out;
	}

	sendRequestForSearch = debounce(function (query, maxResult) {
		BooksAPI.search(query, maxResult).then(res => {
			if (query.length && !res.error) {
				this.setState({ bookResults: res })
			} else {
				this.resetSearchResultState();
			}
		}).catch(this.resetSearchResultState);
	}, 1000, { leading: false, trailing: true });

	searchEveryBook = (query) => {
		const maxResult = 7;
		if (query.length) {
			this.sendRequestForSearch(query, maxResult);
		} else {
			this.resetSearchResultState();
		}
	}

	render() {
		return (

				<switch>
					<Route path="/search" render={() => (

							<SearchBook
								searchEveryBook={this.searchEveryBook}
							/>
							<SearchResults
								changeBookShelf={this.changeBookShelf}
								bookResults={this.state.bookResults}
								booksInShelf={this.state.books}
								resetSearchResultState={this.resetSearchResultState}
							/>

					)} />

					<Route path="/" exact render={() => (

MyReads

									{
										['Currently Reading', 'Read', 'Want to Read'].map(shelf =>
											<BookShelf
												key={shelf}
												shelfName={shelf}
												books={
													this.state.books.filter(
														book => book.shelf === this.convertToCamelCase(shelf)
													)
												}
												booksInShelf={this.state.books}
												changeBookShelf={this.changeBookShelf}
											/>
										)
									}

								<Link to="/search">Add a book</Link>

					)}
					/>

					<Route component={Page404} />
				</switch>

		);
	}
}

export default App;
<commit_msg>AKS: done<commit_after>import React from "react";
import { Switch, Link, Route } from "react-router-dom";
import debounce from 'lodash.debounce'

import SearchBook from "./SearchBook";
import BookShelf from "./BookShelf";
import SearchResults from "./SearchResults"
import Page404 from "./Page404"

import * as BooksAPI from "./BooksAPI";
import "./App.css";

class App extends React.Component {
	state = {
		books: [],
		bookResults: []
	};

	componentDidMount() {
		BooksAPI.getAll().then(books => {
			this.setState({ books });
		});
	}

	setBooksAgain() {
		BooksAPI.getAll().then(books => {
			this.setState({ books });
		});
	}

	changeBookShelf = (book_to_change, event) => {
		BooksAPI.update(book_to_change, event.target.value).then(result => {
			this.setBooksAgain();
		});
	};

	updateBook = (book, shelf) => {
		BooksAPI.update(book, shelf).then(res => {
			this.setBooksAgain()
		})
	}

	resetSearchResultState = () => {
		this.setState({ bookResults: [] });
	}

	convertToCamelCase = (str) => {
		var out = "";
		str.split(" ").forEach(function (el, idx) {
			var add = el.toLowerCase();
			out += (idx === 0 ? add : add[0].toUpperCase() + add.slice(1));
		});
		return out;
	}

	sendRequestForSearch = debounce(function (query, maxResult) {
		BooksAPI.search(query, maxResult).then(res => {
			if (query.length && !res.error) {
				this.setState({ bookResults: res })
			} else {
				this.resetSearchResultState();
			}
		}).catch(this.resetSearchResultState);
	}, 1000, { leading: false, trailing: true });

	searchEveryBook = (query) => {
		const maxResult = 7;
		if (query.length) {
			this.sendRequestForSearch(query, maxResult);
		} else {
			this.resetSearchResultState();
		}
	}

	render() {
		return (

				<Switch>
					<Route path="/" exact render={() => (

MyReads

									{
										['Currently Reading', 'Read', 'Want to Read'].map(shelf =>
											<BookShelf
												key={shelf}
												shelfName={shelf}
												books={
													this.state.books.filter(
														book => book.shelf === this.convertToCamelCase(shelf)
													)
												}
												booksInShelf={this.state.books}
												changeBookShelf={this.changeBookShelf}
											/>
										)
									}

								<Link to="/search">Add a book</Link>

					)}
					/>

					<Route path="/search" render={() => (

							<SearchBook
								searchEveryBook={this.searchEveryBook}
							/>
							<SearchResults
								changeBookShelf={this.changeBookShelf}
								bookResults={this.state.bookResults}
								booksInShelf={this.state.books}
								resetSearchResultState={this.resetSearchResultState}
							/>

					)} />

					<Route component={Page404} />
				</Switch>

		);
	}
}

export default App;
<|endoftext|>


<commit_before>'use strict';

angular.module('app')
    .controller('rgiDeleteUserDialogCtrl', function (
        $scope,
        $location,
        ngDialog,
        rgiUserMethodSrvc,
        rgiNotifier,
        rgiIdentitySrvc
    ) {
        $scope.current_user = rgiIdentitySrvc.currentUser;
        $scope.userDelete = function () {
            var user_deletion = $scope.$parent.user;

            if (user_deletion.assessments.length > 0) {
                rgiNotifier.error('You cannot delete a user with an assigned assessment!');
            } else if (user_deletion.isSupervisor()) {
                rgiNotifier.error('You cannot delete a supervisor!');
            } else {
                rgiUserMethodSrvc.deleteUser(user_deletion._id).then(function () {
                    $scope.closeDialog();
                    $location.path('/admin/user-admin');
                    rgiNotifier.notify('User account has been deleted');
                }, function (reason) {
                    rgiNotifier.error(reason);
                });
            }
        };
        $scope.closeDialog = function () {
            ngDialog.close();
        };
    });<commit_msg>allow supervisor removal<commit_after>'use strict';

angular.module('app')
    .controller('rgiDeleteUserDialogCtrl', function (
        $scope,
        $location,
        ngDialog,
        rgiUserMethodSrvc,
        rgiNotifier,
        rgiIdentitySrvc
    ) {
        $scope.current_user = rgiIdentitySrvc.currentUser;
        $scope.userDelete = function () {
            var user_deletion = $scope.$parent.user;

            if (user_deletion.assessments.length > 0) {
                rgiNotifier.error('You cannot delete a user with an assigned assessment!');
            } else {
                rgiUserMethodSrvc.deleteUser(user_deletion._id).then(function () {
                    $scope.closeDialog();
                    $location.path('/admin/user-admin');
                    rgiNotifier.notify('User account has been deleted');
                }, function (reason) {
                    rgiNotifier.error(reason);
                });
            }
        };
        $scope.closeDialog = function () {
            ngDialog.close();
        };
    });<|endoftext|>


<commit_before>(function($) {
  "use strict";

  CodeMirror.registerHelper("hint", "sumo", function(editor, options) {
    options = $.extend({}, {word: /[\w$-]+/, range: 500}, options);
    var cur = editor.getCursor(), curLine = editor.getLine(cur.line);
    var end = cur.ch, start = end;
    while (start && word.test(curLine.charAt(start - 1))) --start;
    var curWord = start != end && curLine.slice(start, end);

    var list = [], seen = {};
    var re = new RegExp(word.source, "g");
    for (var dir = -1; dir <= 1; dir += 2) {
      var line = cur.line, endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir;
      for (; line != endLine; line += dir) {
        var text = editor.getLine(line), m;
        while (m = re.exec(text)) {
          if (line == cur.line && m[0] === curWord) continue;
          if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) {
            seen[m[0]] = true;
            list.push(m[0]);
          }
        }
      }
    }
    return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)};
  });
})(jQuery);
<commit_msg>eslinted<commit_after>/*global CodeMirror, jQuery */

(function($) {
  "use strict";

  CodeMirror.registerHelper("hint", "sumo", function(editor, options) {
    options = $.extend({}, {word: /[\w$-]+/, range: 500}, options);

    var word = options.word;
    var range = options.range;
    var cur = editor.getCursor();
    var curLine = editor.getLine(cur.line);
    var end = cur.ch;
    var start = end;

    while (start && word.test(curLine.charAt(start - 1))) {
      --start;
    }

    var curWord = (start !== end && curLine.slice(start, end));
    var list = [];
    var seen = {};
    var re = new RegExp(word.source, "g");

    for (var dir = -1; dir <= 1; dir += 2) {
      var line = cur.line;
      var endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir;

      for (; line !== endLine; line += dir) {
        var text = editor.getLine(line);
        var m = re.exec(text);

        while (m) {
          if (line === cur.line && m[0] === curWord) { continue; }
          if ((!curWord || m[0].lastIndexOf(curWord, 0) === 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) {
            seen[m[0]] = true;
            list.push(m[0]);
          }
          m = re.exec(text);
        }
      }
    }
    return {list: list, from: new CodeMirror.Pos(cur.line, start), to: new CodeMirror.Pos(cur.line, end)};
  });
})(jQuery);
<|endoftext|>


<commit_before>var childProcess = require('child_process');
const fs = require('fs');
const path = require('path');

function ffmpegPath() {
  return path.join(__dirname, 'compiled', 'ffmpeg_mac');
}

export default function encodeAudio(srcPath, destDir, handleChange) {
  var outPath = path.join(destDir, 'media', path.parse(srcPath).name + '.mp3');
  var opts = [
    '-i',
    srcPath,
    '-codec:a',
    'libmp3lame',
    '-b:a',
    '192k',
    outPath
  ];
  var ff = childProcess.spawn(ffmpegPath(), opts);
  ff.on('exit', (code, signal) => {
    var stats = fs.statSync(outPath);
    handleChange('fileurl', outPath);
    handleChange('filesize', stats.size);
    handleChange('filetype', 'audio/mpeg');
  });
}
<commit_msg>perform 2-pass encoding normalizing audio to -18 LUFS<commit_after>var childProcess = require('child_process');
const fs = require('fs');
const path = require('path');

function ffmpegPath() {
  return path.join(__dirname, 'compiled', 'ffmpeg_mac2');
}

function measureLoudness(srcPath, destDir, handleChange) {
  var opts = [
    '-i',
    srcPath,
    '-af',
    'loudnorm=I=-18:TP=-0.5:LRA=9:print_format=json',
    '-f',
    'null',
    '-'
  ];
  var stderrText = '';
  var ff = childProcess.spawn(ffmpegPath(), opts);
  ff.stderr.on('data', function(data) {
    stderrText += data;
  });
  ff.on('exit', (code, signal) => {
    var jsonStartIndex = stderrText.lastIndexOf('{');
    var jsonString = stderrText.slice(jsonStartIndex);
    var measuredJson = JSON.parse(jsonString);
    console.log(measuredJson);
    secondPass(srcPath, destDir, measuredJson, handleChange);
  });
}

function secondPass(srcPath, destDir, loudnessInfo, handleChange) {
  var outPath = path.join(destDir, 'media', path.parse(srcPath).name + '.mp3');
  var afString = 'measured_I=' + loudnessInfo.input_i +
    ':measured_LRA=' + loudnessInfo.input_lra +
    ':measured_TP=' + loudnessInfo.input_tp +
    ':measured_thresh=' + loudnessInfo.input_thresh +
    ':offset=' + loudnessInfo.target_offset +
    ':linear=true:print_format=summary';
  var opts = [
    '-i',
    srcPath,
    '-af',
    'loudnorm=I=-18:TP=-0.5:LRA=9:' + afString,
    '-codec:a',
    'libmp3lame',
    '-b:a',
    '192k',
    outPath
  ];
  var ff = childProcess.spawn(ffmpegPath(), opts);
  ff.on('exit', (code, signal) => {
    var stats = fs.statSync(outPath);
    handleChange('fileurl', outPath);
    handleChange('filesize', stats.size);
    handleChange('filetype', 'audio/mpeg');
  });
}

export default function encodeAudio(srcPath, destDir, handleChange) {
  measureLoudness(srcPath, destDir, handleChange);
}
<|endoftext|>


<commit_before>/*jslint node: true*/
//	Provides methods which may be executed from the command prompt by being in this files cwd.
//	Type grunt to run the default method, or "grunt paramater" to run a specific method.
//
//	Options:
//		*	grunt: Start up a server, run Jasmine test cases, watch for changes.
//		*	grunt test: Start up a server, run Jasmine test cases.
//		*	grunt lint: Display linter errors about the project
//		*	grunt dist: Create a dist folder with a .zip containing the extension ready to be uploaded
//
//	See here for more information: http://gruntjs.com/sample-gruntfile
'use strict';

module.exports = function (grunt) {

	grunt.initConfig({

		//	Read project settings from package.json in order to be able to reference the properties with grunt.
		pkg: grunt.file.readJSON('package.json'),

	    //	Tasks:

	    //  Prepare CSS for deployment by combining into larger files.
	    //  More options are applied by useminPrepare's evaluation of .html files
		concat: {
		    options: {
		        separator: ';',
		        stripBanners: true
		    }
		},

		htmlmin: {
		    dist: {
		        options: {
		            removeComments: true,
		            collapseWhitespace: true,
		            collapseBooleanAttributes: true,
		            removeAttributeQuotes: true,
		            removeRedundantAttributes: true,
		            useShortDoctype: true,
		            removeEmptyAttributes: true,
		            removeOptionalTags: true
		        },
		        expand: true,
		        cwd: 'dist',
		        dest: 'dist/',
		        src: ['**/*.html']
		    }
		},

		//	Connect spins up tiny, quick servers for testing on
		connect: {
			test: {
				port: 8000
			}
		},

		//	Jasmine is for running our test cases. This runs in a headless web browser using phantom-js which is pretty sweet.
		jasmine: {
			//	Here's all the JavaScript I want to consider when running test cases
			src: 'app/js/*.js',
			options: {
				//	Specs are all the cases I want to run
				specs: 'test/js/spec/*Spec.js',
				//	Don't run under file:// because some APIs don't respond well to that
				host: 'http://localhost:8000/',
				template: require('grunt-template-jasmine-requirejs'),
				templateOptions: {
					requireConfigFile: 'test/js/main.js',
					requireConfig: {
						//	Override the base URL with one relative to the gruntfile.
						baseUrl: 'app/js/',
						//	Emulate main.js' initialization logic.
						deps: ['settings', 'backbone', 'jquery','lodash'],
						callback: function (Settings, Backbone, $, _) {
							//	Enable testing in Settings so configuration values can be set accordingly (API keys, etc. testing runs on localhost)
							Settings.set('testing', true);
							//	Testing should hit a local server and not be ran against the production database.
							Settings.set('localDebug', true);
						}
					}
				}
			}
		},

		//	Improve code quality by applying a code-quality check with jshint
		jshint: {
			//	Files to analyze:
			files: ['Gruntfile.js', 'app/js/**/*.js', 'test/js/**/*.js'],

			options: {
				//	Override JSHint defaults for the extension
				globals: {
					jQuery: true,
					console: true
				},

                //  TODO: I'd like to remove this relaxation from the linter at some point.
				"eqnull": true,

				//	Don't validate third-party libraries
				ignores: ['app/js/thirdParty/**/*.js']
			}
		},

		requirejs: {
		    production: {
		        options: {
		            appDir: 'src',
		            dir: 'dist/',
		            //  Inlines the text for any text! dependencies, to avoid the separate
		            //  async XMLHttpRequest calls to load those dependencies.
		            inlineText: true,
		            stubModules: ['text'],
		            useStrict: true,
		            mainConfigFile: 'src/js/requireConfig.js',
		            //  List the modules that will be optimized. All their immediate and deep
		            //  dependencies will be included in the module's file when the build is done
                    //  TODO: Options and FullScreen both need updating.
		            //modules: [{
		            //    name: 'background/main',
                    //    include: ['background/plugins']
		            //}, {
		            //    name: 'background/background',
		            //    exclude: ['background/main', 'background/plugins']
		            //}, {
		            //    name: 'foreground/main',
                    //    include: ['foreground/plugins']
		            //}, {
		            //    name: 'foreground/foreground',
		            //    include: ['foreground/view/backgroundDependentForegroundView'],
		            //    exclude: ['foreground/main']
		            //}],
		            modules: [{
		                name: 'background/main'
		            }, {
                        name: 'foreground/main'
		            }],
		            findNestedDependencies: true,
		            optimize: 'uglify2',
		            //  Skip CSS optimizations in RequireJS step -- handle with cssmin because it supports multiple CSS files.
		            optimizeCss: 'none',
		            preserveLicenseComments: false,
		            //  Don't leave a copy of the file if it has been concatenated into a larger one.
		            removeCombined: true,
		            //  Skip files which start with a . or end in vs-doc.js
		            fileExclusionRegExp: /^\.|vsdoc.js$|.css$/
		        }

			}
		},

        //  TODO: Is this actually minifying? The files still seem abnormally large.
		uglify: {
		    inject: {
		        files: {
		            'dist/js/inject/beatportInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/bootstrap.min.js', 'src/js/inject/beatportInject.js'],
		            'dist/js/inject/streamusInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/streamusInject.js'],
		            'dist/js/inject/streamusShareInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/streamusShareInject.js'],
		            'dist/js/inject/youTubeInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/youTubeInject.js'],
		            'dist/js/inject/youTubeIFrameInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/youTubeIFrameInject.js']
		        }
		    }
		},

		useminPrepare: {
		    //  Target src here so CSS can still be found.
            //  TODO: Options, Fullscreen
		    html: 'src/foreground.html'
		},

		usemin: {
		    html: 'dist/foreground.html'
		},

		watch: {
			files: ['<%= jshint.files %>'],
			tasks: ['jshint']
		}
	});

	grunt.loadNpmTasks('grunt-contrib-clean');
	grunt.loadNpmTasks('grunt-contrib-concat');
	grunt.loadNpmTasks('grunt-contrib-cssmin');
	grunt.loadNpmTasks('grunt-contrib-compress');
	grunt.loadNpmTasks('grunt-contrib-connect');
	grunt.loadNpmTasks('grunt-contrib-copy');
	grunt.loadNpmTasks('grunt-contrib-htmlmin');
    //  Bulky, install on-demand only.
    //grunt.loadNpmTasks('grunt-contrib-imagemin');
	grunt.loadNpmTasks('grunt-contrib-jasmine');
	grunt.loadNpmTasks('grunt-contrib-jshint');
	grunt.loadNpmTasks('grunt-contrib-requirejs');
	grunt.loadNpmTasks('grunt-contrib-uglify');
	grunt.loadNpmTasks('grunt-usemin');
	grunt.loadNpmTasks('grunt-contrib-watch');
	grunt.loadNpmTasks('grunt-template-jasmine-requirejs');
	grunt.loadNpmTasks('grunt-text-replace');

	grunt.registerTask('default', ['connect jasmine watch']);
	grunt.registerTask('test', ['connect', 'jasmine']);
    //  TODO: enable htmlmin once I figure out why my templates aren't being parsed.
	grunt.registerTask('production', ['requirejs', 'uglify', 'dist-manifest-transform', 'update-require-config-paths', 'useminPrepare', 'usemin', 'concat', 'cssmin']);
	//grunt.registerTask('production', ['requirejs']);
	grunt.registerTask('lint', ['jshint']);

	//	Generate a versioned zip file after transforming relevant files to production-ready versions.
	grunt.registerTask('dist', 'Cleanup the extension and zip it up.', function (version) {

		//	Update version number in manifest.json:
		if (version === undefined) {
			grunt.warn('dist must be called with a version. e.g.: dist:0.98');
			return;
		}

		grunt.option('version', version);

		grunt.task.run('dist-update-manifest-version', 'dist-remove-old-folder', 'dist-copy-source', 'dist-remove-undeployable-files', 'dist-manifest-transform', 'dist-transform-settings', 'dist-compress');
	});

	//	Update the manifest file's version number first -- new version is being distributed and it is good to keep files all in sync.
	grunt.registerTask('dist-update-manifest-version', 'updates the manifest version to the to-be latest distributed version', function () {
		grunt.config.set('replace', {
			updateManifestVersion: {
				src: ['app/manifest.json'],
				overwrite: true,
				replacements: [{
					from: /"version": "\d{0,3}.\d{0,3}"/,
					to: '"version": "' + grunt.option('version') + '"'
				}]
			}
		});
		grunt.task.run('replace');
	});

	//	Cleanup any old dist folder to ensure only the files we want are put into the current release.
	//grunt.registerTask('dist-remove-old-folder', 'removes any previously existing dist folder', function () {
	//	if (grunt.file.exists('dist')) {
	//		//	Can't delete a full directory -- clean it up.
	//		grunt.config.set('clean', ['dist']);
	//		grunt.task.run('clean');
	//		grunt.file.delete('dist');
	//	}
	//});

	//	Copy everything before transforming to not affect the originals.
	grunt.registerTask('dist-copy-source', 'copy all the needed files to the dist directory', function () {

		grunt.config.set('copy', {
			copyExtension: {
				expand: true,
				src: 'app/**',
				dest: 'dist'
			}
		});
		grunt.task.run('copy');

	});

	grunt.registerTask('dist-remove-undeployable-files', 'removes any files that should not be deployed', function () {
		grunt.file.delete('dist/app/js/thirdParty/chrome-api-vsdoc.js');
		grunt.file.delete('dist/app/js/thirdParty/jasmine.js');
		grunt.file.delete('dist/app/js/thirdParty/jasmine-html.js');
	});

    //  TODO: This is probably bad practice.
	grunt.registerTask('update-require-config-paths', 'changes the paths for require config so they work for deployment', function () {

	    grunt.config.set('replace', {
	        removeDebuggingKeys: {
	            src: ['dist/js/background/main.js', 'dist/js/foreground/main.js'],
	            overwrite: true,
	            replacements: [{
	                //  Change all main files paths to requireConfig for to be accurate for deployment.
	                from: '../requireConfig',
	                to: 'requireConfig'
	            }]
	        }
	    });

	    grunt.task.run('replace');
	});

	//	Remove debugging information from the manifest file
	grunt.registerTask('dist-manifest-transform', 'removes debugging info from the manifest.json', function () {

		grunt.config.set('replace', {
			removeDebuggingKeys: {
				src: ['dist/manifest.json'],
				overwrite: true,
				replacements: [{
					//	Remove manifest key -- can't upload to Chrome Web Store if this entry exists in manifest.json, but helps with debugging.
					from: /"key".*/,
					to: ''
				}, {
					//	Remove permissions that're only needed for debugging.
					from: '"http://localhost:61975/Streamus/",',
					to: ''
				}, {
				    //  Transform inject javascript to reference uglified/concat versions for deployment.
				    from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/youTubeIFrameInject.js"]',
				    to: '"js": ["js/inject/youTubeIFrameInject.js"]'
				}, {
				    //  Transform inject javascript to reference uglified/concat versions for deployment.
				    from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/youTubeInject.js"]',
				    to: '"js": ["js/inject/youTubeInject.js"]'
				}, {
				    //  Transform inject javascript to reference uglified/concat versions for deployment.
				    from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/streamusShareInject.js"]',
				    to: '"js": ["js/inject/streamusShareInject.js"]'
				}, {
				    //  Transform inject javascript to reference uglified/concat versions for deployment.
				    from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/streamusInject.js"]',
				    to: '"js": ["js/inject/streamusInject.js"]'
				}, {
				    //  Transform inject javascript to reference uglified/concat versions for deployment.
				    from: '"js": ["js/thirdParty/jquery.js", "js/thirdParty/bootstrap.min.js", "js/inject/beatportInject.js"]',
				    to: '"js": ["js/inject/beatportInject.js"]'
				}]
			}
		});

		grunt.task.run('replace');
	});

	//	Remove debugging information from the JavaScript
	grunt.registerTask('dist-transform-settings', 'ensure all the debugging flags are turned off in settings', function () {

		grunt.config.set('replace', {
			transformSettings: {
				src: ['dist/app/js/background/model/settings.js'],
				overwrite: true,
				replacements: [{
					//	Find the line that looks like: "localDebug: true" and set it to false. Local debugging is for development only.
					from: 'localDebug: true',
					to: 'localDebug: false'
				}, {
					//	Find the line that looks like: "testing: true" and set it to false. Testing is for development only.
					from: 'testing: true',
					to: 'testing: false'
				}]
			}
		});

		grunt.task.run('replace');

	});

	//	Zip up the dist folder
	grunt.registerTask('dist-compress', 'compress the files which are ready to be uploaded to the Chrome Web Store into a .zip', function () {

		var zipFileName = 'Streamus v' + grunt.option('version') + '.zip';

		//	Remove old version if it exists
		if (grunt.file.exists(zipFileName)) {
			grunt.file.delete(zipFileName);
		}

		grunt.config.set('compress', {
			dist: {

				options: {
					archive: zipFileName
				},
				files: [{
					src: ['dist/app/**'],
					dest: 'streamus/'
				}]

			}
		});

		grunt.task.run('compress');

	});

};<commit_msg>updated grunt<commit_after>/*jslint node: true*/
//	Provides methods which may be executed from the command prompt by being in this files cwd.
//	Type grunt to run the default method, or "grunt paramater" to run a specific method.
//
//	Options:
//		*	grunt: Start up a server, run Jasmine test cases, watch for changes.
//		*	grunt test: Start up a server, run Jasmine test cases.
//		*	grunt lint: Display linter errors about the project
//		*	grunt production: Create a dist folder with a .zip containing the extension ready to be uploaded
//
//	See here for more information: http://gruntjs.com/sample-gruntfile
'use strict';

module.exports = function (grunt) {

	grunt.initConfig({

		//	Read project settings from package.json in order to be able to reference the properties with grunt.
		pkg: grunt.file.readJSON('package.json'),

		//	Tasks:
		concat: {
			//  NOTE: Careful not to define separator as semi-colon here. It will error out on font-awesome CSS.
			options: {
				stripBanners: true
			}
		},

		htmlmin: {
			dist: {
				options: {
					removeComments: true,
					collapseWhitespace: true,
					collapseBooleanAttributes: true,
					removeAttributeQuotes: true,
					removeRedundantAttributes: true,
					useShortDoctype: true,
					removeEmptyAttributes: true,
					removeOptionalTags: true
				},
				expand: true,
				cwd: 'dist',
				dest: 'dist/',
				//  Don't minify template files becuase they can't be reliably parsed w/ javascript injected.
				src: ['**/*.html', '!**/template/**']
			}
		},

		//	Connect spins up tiny, quick servers for testing on
		connect: {
			test: {
				port: 8000
			}
		},

		//  Compress image sizes and move to dist folder
		imagemin: {

			dynamic: {
				files: [{
					expand: true,
					cwd: 'dist/img',
					src: ['**/*.{png,jpg,gif}'],
					dest: 'dist/img/'
				}]
			}

		},

		//	Jasmine is for running our test cases. This runs in a headless web browser using phantom-js which is pretty sweet.
		jasmine: {
			//	Here's all the JavaScript I want to consider when running test cases
			src: 'src/js/*.js',
			options: {
				//	Specs are all the cases I want to run
				specs: 'test/js/spec/*Spec.js',
				//	Don't run under file:// because some APIs don't respond well to that
				host: 'http://localhost:8000/',
				template: require('grunt-template-jasmine-requirejs'),
				templateOptions: {
					requireConfigFile: 'test/js/main.js',
					requireConfig: {
						//	Override the base URL with one relative to the gruntfile.
						baseUrl: 'src/js/',
						//	Emulate main.js' initialization logic.
						deps: ['settings', 'backbone', 'jquery','lodash'],
						callback: function (Settings, Backbone, $, _) {
							//	Enable testing in Settings so configuration values can be set accordingly (API keys, etc. testing runs on localhost)
							Settings.set('testing', true);
							//	Testing should hit a local server and not be ran against the production database.
							Settings.set('localDebug', true);
						}
					}
				}
			}
		},

		//	Improve code quality by applying a code-quality check with jshint
		jshint: {
			//	Files to analyze:
			files: ['Gruntfile.js', 'src/js/**/*.js', 'test/js/**/*.js'],

			options: {
				//	Override JSHint defaults for the extension
				globals: {
					jQuery: true,
					console: true
				},

				//  TODO: I'd like to remove this relaxation from the linter at some point.
				"eqnull": true,

				//	Don't validate third-party libraries
				ignores: ['src/js/thirdParty/**/*.js']
			}
		},

		requirejs: {
			production: {
				options: {
					appDir: 'src',
					dir: 'dist/',
					//  Inlines the text for any text! dependencies, to avoid the separate
					//  async XMLHttpRequest calls to load those dependencies.
					inlineText: true,
					stubModules: ['text'],
					useStrict: true,
					mainConfigFile: 'src/js/requireConfig.js',
					//  List the modules that will be optimized. All their immediate and deep
					//  dependencies will be included in the module's file when the build is done
					//  TODO: Options and FullScreen both need updating.
					modules: [{
						name: 'background/main',
						include: ['background/plugins']
					}, {
						name: 'background/background',
						exclude: ['background/main', 'background/plugins']
					}, {
						name: 'foreground/main',
						include: ['foreground/plugins']
					}, {
						name: 'foreground/foreground',
						include: ['foreground/view/backgroundDependentForegroundView'],
						exclude: ['foreground/main']
					}],
					optimize: 'uglify2',
					//  Skip CSS optimizations in RequireJS step -- handle with cssmin because it supports multiple CSS files.
					optimizeCss: 'none',
					preserveLicenseComments: false,
					//  Don't leave a copy of the file if it has been concatenated into a larger one.
					removeCombined: true,
					//  Skip files which start with a . or end in vs-doc.js as well as CSS because it is handled by cssmin
					fileExclusionRegExp: /^\.|vsdoc.js$|jasmine.js|jasmine-html.js|.css$|Web|Web.Debug|Web.Release/
				}

			}
		},

		useminPrepare: {
			//  Target src here so CSS can still be found.
			//  TODO: Options, Fullscreen
			html: 'src/foreground.html'
		},

		usemin: {
			html: 'dist/foreground.html'
		},

		watch: {
			files: ['<%= jshint.files %>'],
			tasks: ['jshint']
		}
	});

	grunt.loadNpmTasks('grunt-contrib-clean');
	grunt.loadNpmTasks('grunt-contrib-concat');
	grunt.loadNpmTasks('grunt-contrib-cssmin');
	grunt.loadNpmTasks('grunt-contrib-compress');
	grunt.loadNpmTasks('grunt-contrib-connect');
	grunt.loadNpmTasks('grunt-contrib-htmlmin');
	grunt.loadNpmTasks('grunt-contrib-imagemin');
	grunt.loadNpmTasks('grunt-contrib-jasmine');
	grunt.loadNpmTasks('grunt-contrib-jshint');
	grunt.loadNpmTasks('grunt-contrib-requirejs');
	grunt.loadNpmTasks('grunt-contrib-uglify');
	grunt.loadNpmTasks('grunt-usemin');
	grunt.loadNpmTasks('grunt-contrib-watch');
	grunt.loadNpmTasks('grunt-template-jasmine-requirejs');
	grunt.loadNpmTasks('grunt-text-replace');

	grunt.registerTask('default', ['connect jasmine watch']);
	grunt.registerTask('test', ['connect', 'jasmine']);
	grunt.registerTask('lint', ['jshint']);

	//	Generate a versioned zip file after transforming relevant files to production-ready versions.
	grunt.registerTask('production', 'Transform and copy extension to /dist folder and generate a dist-ready .zip file.', function (version) {

		//	Update version number in manifest.json:
		if (version === undefined) {
			grunt.warn('production must be called with a production version. e.g.: production:0.98');
			return;
		}

		grunt.option('version', version);

		grunt.task.run('lint', 'update-manifest-version', 'requirejs', 'manifest-transform', 'transform-settings', 'concat-uglify-injected-javascript', 'update-require-config-paths', 'useminPrepare', 'usemin', 'concat', 'cssmin', 'htmlmin', 'imagemin', 'cleanup-dist-folder', 'compress-extension');
	});

	//	Update the manifest file's version number first -- new version is being distributed and it is good to keep files all in sync.
	grunt.registerTask('update-manifest-version', 'updates the manifest version to the to-be latest distributed version', function () {
		grunt.config.set('replace', {
			updateManifestVersion: {
				src: ['src/manifest.json'],
				overwrite: true,
				replacements: [{
					from: /"version": "\d{0,3}.\d{0,3}"/,
					to: '"version": "' + grunt.option('version') + '"'
				}]
			}
		});
		grunt.task.run('replace');
	});

	grunt.registerTask('concat-uglify-injected-javascript', 'injected javascript files don\'t use requireJS so they have to be manually concat/uglified', function () {
		//  TODO: Is this actually minifying? The files still seem abnormally large.
		grunt.config.set('uglify', {
			inject: {
				files: {
					'dist/js/inject/beatportInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/bootstrap.min.js', 'src/js/inject/beatportInject.js'],
					'dist/js/inject/streamusInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/streamusInject.js'],
					'dist/js/inject/streamusShareInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/streamusShareInject.js'],
					'dist/js/inject/youTubeInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/youTubeInject.js'],
					'dist/js/inject/youTubeIFrameInject.js': ['src/js/thirdParty/jquery.js', 'src/js/thirdParty/lodash.js', 'src/js/inject/youTubeIFrameInject.js']
				}
			}
		});
		grunt.task.run('uglify');
	});

	grunt.registerTask('cleanup-dist-folder', 'removes the template folder since it was inlined into javascript and deletes build.txt', function () {
		if (grunt.file.exists('dist/template')) {
			//	Can't delete a full directory -- clean it up.
			grunt.config.set('clean', ['dist/template']);
			grunt.task.run('clean');
			grunt.file.delete('dist/template');
		}

		grunt.file.delete('dist/build.txt');
	});

	//  TODO: This is probably bad practice.
	grunt.registerTask('update-require-config-paths', 'changes the paths for require config so they work for deployment', function () {

		grunt.config.set('replace', {
			removeDebuggingKeys: {
				src: ['dist/js/background/main.js', 'dist/js/foreground/main.js'],
				overwrite: true,
				replacements: [{
					//  Change all main files paths to requireConfig for to be accurate for deployment.
					from: '../requireConfig',
					to: 'requireConfig'
				}]
			}
		});

		grunt.task.run('replace');
	});

	//	Remove debugging information from the manifest file
	grunt.registerTask('manifest-transform', 'removes debugging info from the manifest.json', function () {

		grunt.config.set('replace', {
			removeDebuggingKeys: {
				src: ['dist/manifest.json'],
				overwrite: true,
				replacements: [{
					//	Remove manifest key -- can't upload to Chrome Web Store if this entry exists in manifest.json, but helps with debugging.
					from: /"key".*/,
					to: ''
				}, {
					//	Remove permissions that're only needed for debugging.
					from: '"http://localhost:61975/Streamus/",',
					to: ''
				}, {
					//  Transform inject javascript to reference uglified/concat versions for deployment.
					from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/youTubeIFrameInject.js"]',
					to: '"js": ["js/inject/youTubeIFrameInject.js"]'
				}, {
					//  Transform inject javascript to reference uglified/concat versions for deployment.
					from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/youTubeInject.js"]',
					to: '"js": ["js/inject/youTubeInject.js"]'
				}, {
					//  Transform inject javascript to reference uglified/concat versions for deployment.
					from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/streamusShareInject.js"]',
					to: '"js": ["js/inject/streamusShareInject.js"]'
				}, {
					//  Transform inject javascript to reference uglified/concat versions for deployment.
					from: '"js": ["js/thirdParty/lodash.js", "js/thirdParty/jquery.js", "js/inject/streamusInject.js"]',
					to: '"js": ["js/inject/streamusInject.js"]'
				}, {
					//  Transform inject javascript to reference uglified/concat versions for deployment.
					from: '"js": ["js/thirdParty/jquery.js", "js/thirdParty/bootstrap.min.js", "js/inject/beatportInject.js"]',
					to: '"js": ["js/inject/beatportInject.js"]'
				}]
			}
		});

		grunt.task.run('replace');
	});

	//	Remove debugging information from the JavaScript
	grunt.registerTask('transform-settings', 'ensure all the debugging flags are turned off in settings', function () {

		grunt.config.set('replace', {
			transformSettings: {
				src: ['dist/js/background/model/settings.js'],
				overwrite: true,
				replacements: [{
					//	Find the line that looks like: "localDebug: true" and set it to false. Local debugging is for development only.
					from: 'localDebug: true',
					to: 'localDebug: false'
				}, {
					//	Find the line that looks like: "testing: true" and set it to false. Testing is for development only.
					from: 'testing: true',
					to: 'testing: false'
				}]
			}
		});

		grunt.task.run('replace');

	});

	//	Zip up the dist folder
	grunt.registerTask('compress-extension', 'compress the files which are ready to be uploaded to the Chrome Web Store into a .zip', function () {

		var zipFileName = 'Streamus v' + grunt.option('version') + '.zip';

		//	Remove old version if it exists
		if (grunt.file.exists(zipFileName)) {
			grunt.file.delete(zipFileName);
		}

		grunt.config.set('compress', {
			dist: {

				options: {
					archive: zipFileName
				},
				files: [{
					src: ['dist/**'],
					dest: 'streamus/'
				}]

			}
		});

		grunt.task.run('compress');

	});

};<|endoftext|>


<commit_before>import 'rxjs/Observable';
import { ActionsObservable } from 'redux-observable';

import fetch from 'node-fetch';
global.fetch = fetch;

import {
	mockQuery,
	MOCK_APP_STATE,
	MOCK_RENDERPROPS,
	MOCK_ROUTES,
} from 'meetup-web-mocks/lib/app';

import {
	createFakeStore,
	epicIgnoreAction,
} from '../util/testUtils';

import getSyncEpic from '../epics/sync';
import * as syncActionCreators from '../actions/syncActionCreators';
import * as authActionCreators from '../actions/authActionCreators';
import {
	CLICK_TRACK_CLEAR_ACTION,
} from '../actions/clickActionCreators';

jest.mock('react-router', () => ({
	browserHistory: {
		replace: jest.fn(() => {}),
	},
}));

/**
 * @module SyncEpicTest
 */
describe('Sync epic', () => {
	const routes = {};
	it('does not pass through arbitrary actions', epicIgnoreAction(getSyncEpic(MOCK_ROUTES)));
	it('emits API_REQUEST and CLICK_TRACK_CLEAR for nav-related actions with matched query', function() {
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: MOCK_RENDERPROPS.location };
		const serverRender = { type: '@@server/RENDER', payload: MOCK_RENDERPROPS.location };

		const fakeStore = createFakeStore({});
		const action$ = ActionsObservable.of(locationChange, serverRender);
		return getSyncEpic(MOCK_ROUTES)(action$, fakeStore)
			.toArray()
			.toPromise()
			.then(actions => {
				const types = actions.map(a => a.type);
				expect(types.includes('API_REQUEST')).toBe(true);
				expect(types.includes(CLICK_TRACK_CLEAR_ACTION)).toBe(true);
			});
	});
	it('emits API_REQUEST, CACHE_CLEAR, and CLICK_TRACK_CLEAR for nav-related actions with logout query', function() {
		const logoutLocation = {
			...MOCK_RENDERPROPS.location,
			search: '?foo=bar&logout=true',
		};
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: logoutLocation };

		const fakeStore = createFakeStore({});
		const action$ = ActionsObservable.of(locationChange);
		return getSyncEpic(MOCK_ROUTES)(action$, fakeStore)
			.toArray()
			.toPromise()
			.then(actions => {
				const types = actions.map(a => a.type);
				expect(types.includes('API_REQUEST')).toBe(true);
				expect(types.includes('CACHE_CLEAR')).toBe(true);
				expect(types.includes(CLICK_TRACK_CLEAR_ACTION)).toBe(true);
			});
	});
	it('does not emit for nav-related actions without matched query', () => {
		const SyncEpic = getSyncEpic(MOCK_ROUTES);

		const pathname = '/noQuery';
		const noMatchLocation = { ...MOCK_RENDERPROPS.location, pathname };
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: noMatchLocation };
		const serverRender = { type: '@@server/RENDER', payload: noMatchLocation };

		return epicIgnoreAction(SyncEpic, locationChange)()
			.then(epicIgnoreAction(SyncEpic, serverRender));
	});
	it('does not emit for nav-related actions with query functions that return null', () => {
		const SyncEpic = getSyncEpic(MOCK_ROUTES);

		const pathname = '/nullQuery';
		const noMatchLocation = { ...MOCK_RENDERPROPS.location, pathname };
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: noMatchLocation };
		const serverRender = { type: '@@server/RENDER', payload: noMatchLocation };

		return epicIgnoreAction(SyncEpic, locationChange)()
			.then(epicIgnoreAction(SyncEpic, serverRender));
	});

	xit('strips logout query and calls browserHistory.replace on LOGIN_SUCCESS', function() {
		const mockFetchQueries = () => () => Promise.resolve({});
		const locationWithLogout = {
			...MOCK_APP_STATE.routing.locationBeforeTransitions,
			query: { logout: true },
		};
		const locationWithoutLogout = {
			...locationWithLogout,
			query: {},
		};
		const MOCK_APP_STATE_LOGOUT = {
			...MOCK_APP_STATE,
			routing: {
				locationBeforeTransitions: locationWithLogout
			}
		};

		const locationSync = authActionCreators.loginSuccess();
		const action$ = ActionsObservable.of(locationSync);
		const fakeStore = createFakeStore(MOCK_APP_STATE_LOGOUT);
		return getSyncEpic(routes, mockFetchQueries)(action$, fakeStore)
			.toPromise()
			.then(() => {
				expect(require('react-router').browserHistory.replace).toHaveBeenCalledWith(locationWithoutLogout);
			});
	});

	it('emits API_SUCCESS and API_COMPLETE on successful API_REQUEST', function() {
		const mockFetchQueries = () => () => Promise.resolve({});

		const queries = [mockQuery({})];
		const apiRequest = syncActionCreators.apiRequest(queries);
		const action$ = ActionsObservable.of(apiRequest);
		const fakeStore = createFakeStore(MOCK_APP_STATE);
		return getSyncEpic(routes, mockFetchQueries)(action$, fakeStore)
			.toArray()
			.toPromise()
			.then(actions =>
				expect(actions.map(({ type }) => type)).toEqual(['API_SUCCESS', 'API_COMPLETE'])
			);
	});

	it('emits API_ERROR on failed API_REQUEST', function() {
		const mockFetchQueries = () => () => Promise.reject(new Error());

		const queries = [mockQuery({})];
		const apiRequest = syncActionCreators.apiRequest(queries);
		const action$ = ActionsObservable.of(apiRequest);
		const fakeStore = createFakeStore(MOCK_APP_STATE);
		return getSyncEpic(routes, mockFetchQueries)(action$, fakeStore)
			.toPromise()
			.then(action => expect(action.type).toEqual('API_ERROR'));
	});

});

<commit_msg>remove jest mock for react-router<commit_after>import 'rxjs/Observable';
import { ActionsObservable } from 'redux-observable';

import fetch from 'node-fetch';
global.fetch = fetch;

import {
	mockQuery,
	MOCK_APP_STATE,
	MOCK_RENDERPROPS,
	MOCK_ROUTES,
} from 'meetup-web-mocks/lib/app';

import {
	createFakeStore,
	epicIgnoreAction,
} from '../util/testUtils';

import getSyncEpic from '../epics/sync';
import * as syncActionCreators from '../actions/syncActionCreators';
import * as authActionCreators from '../actions/authActionCreators';
import {
	CLICK_TRACK_CLEAR_ACTION,
} from '../actions/clickActionCreators';

/**
 * @module SyncEpicTest
 */
describe('Sync epic', () => {
	const routes = {};
	it('does not pass through arbitrary actions', epicIgnoreAction(getSyncEpic(MOCK_ROUTES)));
	it('emits API_REQUEST and CLICK_TRACK_CLEAR for nav-related actions with matched query', function() {
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: MOCK_RENDERPROPS.location };
		const serverRender = { type: '@@server/RENDER', payload: MOCK_RENDERPROPS.location };

		const fakeStore = createFakeStore({});
		const action$ = ActionsObservable.of(locationChange, serverRender);
		return getSyncEpic(MOCK_ROUTES)(action$, fakeStore)
			.toArray()
			.toPromise()
			.then(actions => {
				const types = actions.map(a => a.type);
				expect(types.includes('API_REQUEST')).toBe(true);
				expect(types.includes(CLICK_TRACK_CLEAR_ACTION)).toBe(true);
			});
	});
	it('emits API_REQUEST, CACHE_CLEAR, and CLICK_TRACK_CLEAR for nav-related actions with logout query', function() {
		const logoutLocation = {
			...MOCK_RENDERPROPS.location,
			search: '?foo=bar&logout=true',
		};
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: logoutLocation };

		const fakeStore = createFakeStore({});
		const action$ = ActionsObservable.of(locationChange);
		return getSyncEpic(MOCK_ROUTES)(action$, fakeStore)
			.toArray()
			.toPromise()
			.then(actions => {
				const types = actions.map(a => a.type);
				expect(types.includes('API_REQUEST')).toBe(true);
				expect(types.includes('CACHE_CLEAR')).toBe(true);
				expect(types.includes(CLICK_TRACK_CLEAR_ACTION)).toBe(true);
			});
	});
	it('does not emit for nav-related actions without matched query', () => {
		const SyncEpic = getSyncEpic(MOCK_ROUTES);

		const pathname = '/noQuery';
		const noMatchLocation = { ...MOCK_RENDERPROPS.location, pathname };
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: noMatchLocation };
		const serverRender = { type: '@@server/RENDER', payload: noMatchLocation };

		return epicIgnoreAction(SyncEpic, locationChange)()
			.then(epicIgnoreAction(SyncEpic, serverRender));
	});
	it('does not emit for nav-related actions with query functions that return null', () => {
		const SyncEpic = getSyncEpic(MOCK_ROUTES);

		const pathname = '/nullQuery';
		const noMatchLocation = { ...MOCK_RENDERPROPS.location, pathname };
		const locationChange = { type: syncActionCreators.LOCATION_CHANGE, payload: noMatchLocation };
		const serverRender = { type: '@@server/RENDER', payload: noMatchLocation };

		return epicIgnoreAction(SyncEpic, locationChange)()
			.then(epicIgnoreAction(SyncEpic, serverRender));
	});

	xit('strips logout query and calls browserHistory.replace on LOGIN_SUCCESS', function() {
		const mockFetchQueries = () => () => Promise.resolve({});
		const locationWithLogout = {
			...MOCK_APP_STATE.routing.locationBeforeTransitions,
			query: { logout: true },
		};
		const locationWithoutLogout = {
			...locationWithLogout,
			query: {},
		};
		const MOCK_APP_STATE_LOGOUT = {
			...MOCK_APP_STATE,
			routing: {
				locationBeforeTransitions: locationWithLogout
			}
		};

		const locationSync = authActionCreators.loginSuccess();
		const action$ = ActionsObservable.of(locationSync);
		const fakeStore = createFakeStore(MOCK_APP_STATE_LOGOUT);
		return getSyncEpic(routes, mockFetchQueries)(action$, fakeStore)
			.toPromise()
			.then(() => {
				expect(require('react-router').browserHistory.replace).toHaveBeenCalledWith(locationWithoutLogout);
			});
	});

	it('emits API_SUCCESS and API_COMPLETE on successful API_REQUEST', function() {
		const mockFetchQueries = () => () => Promise.resolve({});

		const queries = [mockQuery({})];
		const apiRequest = syncActionCreators.apiRequest(queries);
		const action$ = ActionsObservable.of(apiRequest);
		const fakeStore = createFakeStore(MOCK_APP_STATE);
		return getSyncEpic(routes, mockFetchQueries)(action$, fakeStore)
			.toArray()
			.toPromise()
			.then(actions =>
				expect(actions.map(({ type }) => type)).toEqual(['API_SUCCESS', 'API_COMPLETE'])
			);
	});

	it('emits API_ERROR on failed API_REQUEST', function() {
		const mockFetchQueries = () => () => Promise.reject(new Error());

		const queries = [mockQuery({})];
		const apiRequest = syncActionCreators.apiRequest(queries);
		const action$ = ActionsObservable.of(apiRequest);
		const fakeStore = createFakeStore(MOCK_APP_STATE);
		return getSyncEpic(routes, mockFetchQueries)(action$, fakeStore)
			.toPromise()
			.then(action => expect(action.type).toEqual('API_ERROR'));
	});

});

<|endoftext|>


<commit_before>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import readResponseBlob from '../../utils/readResponseBlob';
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';

export default () => describe('save', () => {
  let dashboardId;
  let boxplotChartId;

  beforeEach(() => {
    cy.server();
    cy.login();
    cy.visit(WORLD_HEALTH_DASHBOARD);

    cy.get('#app').then((data) => {
      const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
      const dashboard = bootstrapData.dashboard_data;
      dashboardId = dashboard.id;
      boxplotChartId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'box_plot')).slice_id;

      cy.route('POST', `/superset/copy_dash/${dashboardId}/`).as('copyRequest');
    });

    cy.get('#save-dash-split-button').trigger('click', { force: true });
    cy.contains('Save as').trigger('click', { force: true });
    cy.get('.modal-footer').contains('Save').trigger('click', { force: true });
  });

  it('should save as new dashboard', () => {
    cy.wait('@copyRequest').then((xhr) => {
      expect(xhr.status).to.eq(200);

      readResponseBlob(xhr.response.body).then((json) => {
        expect(json.id).to.be.gt(dashboardId);
      });
    });
  });

  it('should save/overwrite dashboard', () => {
    cy.wait('@copyRequest');

    // should have box_plot chart
    const formData = `{"slice_id":${boxplotChartId}}`;
    const boxplotRequest = `/superset/explore_json/?form_data=${formData}`;
    cy.route('POST', boxplotRequest).as('boxplotRequest');
    cy.wait('@boxplotRequest');
    cy.get('.grid-container .box_plot').should('be.exist');

    // remove box_plot chart from dashboard
    cy.get('.dashboard-header').contains('Edit dashboard').trigger('click', { force: true });
    cy.get('.fa.fa-trash').last().trigger('click', { force: true });
    cy.get('.grid-container .box_plot').should('not.exist');

    cy.route('POST', '/superset/save_dash/**/').as('saveRequest');
    cy.get('.dashboard-header').contains('Save changes').trigger('click', { force: true });

    // go back to view mode
    cy.wait('@saveRequest');
    cy.get('.dashboard-header').contains('Edit dashboard');
    cy.get('.grid-container .box_plot').should('not.exist');
  });
});
<commit_msg>[cypress] fix accessing a cross-origin frame error (#7552)<commit_after>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import readResponseBlob from '../../utils/readResponseBlob';
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';

export default () => describe('save', () => {
  Cypress.config('chromeWebSecurity', false);

  let dashboardId;
  let boxplotChartId;

  beforeEach(() => {
    cy.server();
    cy.login();
    cy.visit(WORLD_HEALTH_DASHBOARD);

    cy.get('#app').then((data) => {
      const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
      const dashboard = bootstrapData.dashboard_data;
      dashboardId = dashboard.id;
      boxplotChartId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'box_plot')).slice_id;

      cy.route('POST', `/superset/copy_dash/${dashboardId}/`).as('copyRequest');
    });

    cy.get('#save-dash-split-button').trigger('click', { force: true });
    cy.contains('Save as').trigger('click', { force: true });
    cy.get('.modal-footer').contains('Save').trigger('click', { force: true });
  });

  it('should save as new dashboard', () => {
    cy.wait('@copyRequest').then((xhr) => {
      expect(xhr.status).to.eq(200);

      readResponseBlob(xhr.response.body).then((json) => {
        expect(json.id).to.be.gt(dashboardId);
      });
    });
  });

  it('should save/overwrite dashboard', () => {
    cy.wait('@copyRequest');

    // should have box_plot chart
    const formData = `{"slice_id":${boxplotChartId}}`;
    const boxplotRequest = `/superset/explore_json/?form_data=${formData}`;
    cy.route('POST', boxplotRequest).as('boxplotRequest');
    cy.wait('@boxplotRequest');
    cy.get('.grid-container .box_plot').should('be.exist');

    // remove box_plot chart from dashboard
    cy.get('.dashboard-header').contains('Edit dashboard').trigger('click', { force: true });
    cy.get('.fa.fa-trash').last().trigger('click', { force: true });
    cy.get('.grid-container .box_plot').should('not.exist');

    cy.route('POST', '/superset/save_dash/**/').as('saveRequest');
    cy.get('.dashboard-header').contains('Save changes').trigger('click', { force: true });

    // go back to view mode
    cy.wait('@saveRequest');
    cy.get('.dashboard-header').contains('Edit dashboard');
    cy.get('.grid-container .box_plot').should('not.exist');
  });
});
<|endoftext|>


<commit_before>/*jslint unparam: true */

// Selectbox Autocomplete module for MOJ
// Dependencies: moj, jQuery, jQuery UI

(function (window, $) {

  "use strict";

  var MojAutocomplete = function(el, options){
    // avoid applying twice
    if(!el.data("moj.autocomplete")){ // TODO: find a better way...
      // set settings
      this.settings = $.extend({}, this.defaults, options);
      // cache element
      this.$select = el;
      // build new elements
      this._create();

      el.data("moj.autocomplete", true);
    }
  };

  MojAutocomplete.prototype = {
    // default config
    defaults: {
      copyAttr: true,
      autocomplete: {
        delay: 0,
        minLength: 0,
        autoFocus: true
      }
    },

    _create: function(){
      // create and hide el
      this.$wrapper = $("").addClass("moj-autocomplete").insertAfter(this.$select);
      // build autocomplete field
      this._createAutocomplete();
      // hide original select el
      this.$select.hide();
    },

    _createAutocomplete: function(){
      var selected = this.$select.children(":selected"),
          val = selected.val() ? selected.text() : "",
          key, value, i, attrs, raw_attrs;

      this.$text = $( "<input>" ).attr("type", "text") // give it a field type
                                  .val(val) // set value if already selected
                                  .data("select", this.$select); // assoc select with this input

      // if required, copy across attributes - useful for using [placeholder]
      if (this.settings.copyAttr) {
        attrs = {};
        raw_attrs = this.$select[0].attributes;

        for (i=0; i < raw_attrs.length; i++) {
          key = raw_attrs[i].nodeName;
          value = raw_attrs[i].nodeValue;
          if ( key !== "name" && key !== "id" && key !== "class" && typeof this.$select.attr(key) !== "undefined" ) {
            attrs[key] = value;
          }
        }
        this.$text.attr(attrs);
      }

      this.settings.autocomplete.source = $.proxy(this, "_source"); // use source method from class
      // add autocomplete functionality to text field
      this.$text.autocomplete(this.settings.autocomplete);

      // Our items have HTML tags.  The default rendering uses text()
      // to set the content of the  tag.  We need html().
      this.$text.data("ui-autocomplete")._renderItem = function(ul, item) {
        var label = $("").html(item.label);
        if (item.value === -1) {
          label.addClass("ui-menu-noresults");
        }
        return $("
").append(label).appendTo(ul);
      };

      // set callbacks for autocomplete
      this.$text.on({
        autocompleteselect: this._autocompleteselect,
        autocompletechange: this._autocompletechange,
        autocompletesearch: this._autocompletesearch,
        autocompletefocus: this._autocompletefocus
      });

      // append input to wrapper
      this.$wrapper.append(this.$text);
    },

    _source: function(request, response){
      var term = this.stripFormal(request.term),
          matcher = new RegExp("^" + term, "i");

      if (this._hasMatches(request.term)) {
        response(
          this.$select.children("option").map(function() {
            var text = $( this ).text();
            if (this.value && (!request.term || matcher.test(text))){
              return {
                label: request.term !== "" ? text.replace(new RegExp("^(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1") : text,
                value: text,
                option: this
              };
            }
          })
        );
      } else {
        response([{ label: "Prison not found", value: -1}]);
      }
    },

    stripFormal: function (term) {
      var matcher = term.match(/^(hmp?|yoi)? ?/i);
      if (matcher[0].length) {
        term = term.replace(matcher[0], "");
      }
      return $.ui.autocomplete.escapeRegex(term);
    },

    _hasMatches: function (term) {
      var matched = false,
          matcher = new RegExp("^" + this.stripFormal(term) + "[a-z]*", "i");

      this.$select.children("option").each(function() {
        // if a match is found, select it and change to proper case in text field
        if (matcher.test($(this).text().toLowerCase())) {
          matched = true;
        }
      });

      return matched;
    },

    _autocompletefocus: function(event, ui) {
      // don't select 'no results' as value
      if (ui.item.value === -1) {
        event.preventDefault();
      }
    },

    _autocompleteselect: function(event, ui) {
      // don't input '-1' (no results) into text box
      if (ui.item.value === -1) {
        event.preventDefault();
      } else {
        ui.item.option.selected = true;
      }
    },

    _autocompletechange: function(event, ui) {
      var $text = $(this),
          $select = $text.data("select"),
          value = $text.val(),
          valueLowerCase = value.toLowerCase(),
          valid = false;

      // Selected an item, nothing to do
      if (ui.item && ui.item.value !== -1) {
        return;
      }

      // Search for a match (case-insensitive)
      $select.children("option").each(function() {
        // if a match is found, select it and change to proper case in text field
        if ($(this).text().toLowerCase() === valueLowerCase) {
          this.selected = valid = true;
          $text.val($(this).text());
          return false;
        }
      });

      // Found a match, nothing to do
      if (valid) {
        return;
      }

      // Remove invalid value
      $text.val("").attr("title", value + " didn't match any item");
      // clear value from select
      $select.val("");
      // remove value from autocomplete obj
      $text.data("ui-autocomplete").term = "";
    },

    // stop first item being selected when no value has been entered
    _autocompletesearch: function() {
      if($(this).val() === "") {
        $(this).data("ui-autocomplete").options.autoFocus = false;
      } else {
        $(this).data("ui-autocomplete").options.autoFocus = true;
      }
    }
  };

  $.fn.mojAutocomplete = function(options) {
    return this.each(function(){
      new MojAutocomplete($(this), options);
    });
  };

  // attach to window so doesn't have to be used as a jQuery plugin
  window.MojAutocomplete = MojAutocomplete;
}(window, jQuery));

(function(){
  "use strict";

  // Add module to MOJ namespace
  moj.Modules.autocomplete = {
    init: function () {
      // auto initate plugin if class is present
      $(".js-autocomplete").mojAutocomplete($(this).data());
    }
  };
}());
<commit_msg>Consistently use single quotes in JavaScript<commit_after>/*jslint unparam: true */

// Selectbox Autocomplete module for MOJ
// Dependencies: moj, jQuery, jQuery UI

(function (window, $) {

  'use strict';

  var MojAutocomplete = function(el, options){
    // avoid applying twice
    if(!el.data('moj.autocomplete')){ // TODO: find a better way...
      // set settings
      this.settings = $.extend({}, this.defaults, options);
      // cache element
      this.$select = el;
      // build new elements
      this._create();

      el.data('moj.autocomplete', true);
    }
  };

  MojAutocomplete.prototype = {
    // default config
    defaults: {
      copyAttr: true,
      autocomplete: {
        delay: 0,
        minLength: 0,
        autoFocus: true
      }
    },

    _create: function(){
      // create and hide el
      this.$wrapper = $('').addClass('moj-autocomplete').insertAfter(this.$select);
      // build autocomplete field
      this._createAutocomplete();
      // hide original select el
      this.$select.hide();
    },

    _createAutocomplete: function(){
      var selected = this.$select.children(':selected'),
          val = selected.val() ? selected.text() : '',
          key, value, i, attrs, raw_attrs;

      this.$text = $( '<input>' ).attr('type', 'text') // give it a field type
                                  .val(val) // set value if already selected
                                  .data('select', this.$select); // assoc select with this input

      // if required, copy across attributes - useful for using [placeholder]
      if (this.settings.copyAttr) {
        attrs = {};
        raw_attrs = this.$select[0].attributes;

        for (i=0; i < raw_attrs.length; i++) {
          key = raw_attrs[i].nodeName;
          value = raw_attrs[i].nodeValue;
          if ( key !== 'name' && key !== 'id' && key !== 'class' && typeof this.$select.attr(key) !== 'undefined' ) {
            attrs[key] = value;
          }
        }
        this.$text.attr(attrs);
      }

      this.settings.autocomplete.source = $.proxy(this, '_source'); // use source method from class
      // add autocomplete functionality to text field
      this.$text.autocomplete(this.settings.autocomplete);

      // Our items have HTML tags.  The default rendering uses text()
      // to set the content of the  tag.  We need html().
      this.$text.data('ui-autocomplete')._renderItem = function(ul, item) {
        var label = $('').html(item.label);
        if (item.value === -1) {
          label.addClass('ui-menu-noresults');
        }
        return $('
').append(label).appendTo(ul);
      };

      // set callbacks for autocomplete
      this.$text.on({
        autocompleteselect: this._autocompleteselect,
        autocompletechange: this._autocompletechange,
        autocompletesearch: this._autocompletesearch,
        autocompletefocus: this._autocompletefocus
      });

      // append input to wrapper
      this.$wrapper.append(this.$text);
    },

    _source: function(request, response){
      var term = this.stripFormal(request.term),
          matcher = new RegExp('^' + term, 'i');

      if (this._hasMatches(request.term)) {
        response(
          this.$select.children('option').map(function() {
            var text = $( this ).text();
            if (this.value && (!request.term || matcher.test(text))){
              return {
                label: request.term !== '' ? text.replace(new RegExp('^(?![^&;]+;)(?!<[^<>]*)(' + term + ')(?![^<>]*>)(?![^&;]+;)', 'gi'), '$1') : text,
                value: text,
                option: this
              };
            }
          })
        );
      } else {
        response([{ label: 'Prison not found', value: -1}]);
      }
    },

    stripFormal: function (term) {
      var matcher = term.match(/^(hmp?|yoi)? ?/i);
      if (matcher[0].length) {
        term = term.replace(matcher[0], '');
      }
      return $.ui.autocomplete.escapeRegex(term);
    },

    _hasMatches: function (term) {
      var matched = false,
          matcher = new RegExp('^' + this.stripFormal(term) + '[a-z]*', 'i');

      this.$select.children('option').each(function() {
        // if a match is found, select it and change to proper case in text field
        if (matcher.test($(this).text().toLowerCase())) {
          matched = true;
        }
      });

      return matched;
    },

    _autocompletefocus: function(event, ui) {
      // don't select 'no results' as value
      if (ui.item.value === -1) {
        event.preventDefault();
      }
    },

    _autocompleteselect: function(event, ui) {
      // don't input '-1' (no results) into text box
      if (ui.item.value === -1) {
        event.preventDefault();
      } else {
        ui.item.option.selected = true;
      }
    },

    _autocompletechange: function(event, ui) {
      var $text = $(this),
          $select = $text.data('select'),
          value = $text.val(),
          valueLowerCase = value.toLowerCase(),
          valid = false;

      // Selected an item, nothing to do
      if (ui.item && ui.item.value !== -1) {
        return;
      }

      // Search for a match (case-insensitive)
      $select.children('option').each(function() {
        // if a match is found, select it and change to proper case in text field
        if ($(this).text().toLowerCase() === valueLowerCase) {
          this.selected = valid = true;
          $text.val($(this).text());
          return false;
        }
      });

      // Found a match, nothing to do
      if (valid) {
        return;
      }

      // Remove invalid value
      $text.val('').attr('title', value + ' didn\'t match any item');
      // clear value from select
      $select.val('');
      // remove value from autocomplete obj
      $text.data('ui-autocomplete').term = '';
    },

    // stop first item being selected when no value has been entered
    _autocompletesearch: function() {
      if($(this).val() === '') {
        $(this).data('ui-autocomplete').options.autoFocus = false;
      } else {
        $(this).data('ui-autocomplete').options.autoFocus = true;
      }
    }
  };

  $.fn.mojAutocomplete = function(options) {
    return this.each(function(){
      new MojAutocomplete($(this), options);
    });
  };

  // attach to window so doesn't have to be used as a jQuery plugin
  window.MojAutocomplete = MojAutocomplete;
}(window, jQuery));

(function(){
  'use strict';

  // Add module to MOJ namespace
  moj.Modules.autocomplete = {
    init: function () {
      // auto initate plugin if class is present
      $('.js-autocomplete').mojAutocomplete($(this).data());
    }
  };
}());
<|endoftext|>