<commit_before>import React from 'react';
import chai, { expect } from 'chai';
import { merge } from 'lodash';
import chaiEnzyme from 'chai-enzyme';
import { shallow } from 'enzyme';

chai.use(chaiEnzyme());

import Tooltip from '../src/tooltip';

function translateVectorFromTransform(transformString) {
  return transformString.match(/(\d+)/g).map(Number);
}

describe('<Tooltip />', () => {
  describe('basic behavior', () => {
    it('does not render if props.show is false', () => {
      const wrapper = shallow(<Tooltip show={false} />);
      expect(wrapper.type()).to.equal(null);
    });

    it('renders if props.show is true', () => {
      const wrapper = shallow(<Tooltip show />);
      expect(wrapper.type()).to.equal('div');
    });
  });

  describe('position', () => {
    const baseParams = {
      bounds: {
        x: [0, 1200],
        y: [0, 600],
      },
      height: 100,
      mouseX: 300,
      mouseY: 300,
      offsetX: 0,
      offsetY: 0,
      paddingX: 0,
      paddingY: 0,
      width: 100,
    };

    it('positions the tooltip centered around mouseX', () => {
      const position = Tooltip.getPosition(baseParams);
      const [x] = translateVectorFromTransform(position.transform);
      expect(position)
        .to.be.an('object')
        .with.property('transform');
      expect(x).to.equal(250);
    });

    it('positions the tooltip offsetY above/below from mouseY', () => {
      // above
      const positionAbove = Tooltip.getPosition(Object.assign({}, baseParams, { offsetY: 25 }));
      const [, yAbove] = translateVectorFromTransform(positionAbove.transform);
      expect(positionAbove)
        .to.be.an('object')
        .with.property('transform');
      expect(yAbove).to.equal(175);

      // below
      const positionBelow = Tooltip.getPosition(Object.assign({}, baseParams, { offsetY: -25 }));
      const [, yBelow] = translateVectorFromTransform(positionBelow.transform);
      expect(positionBelow)
        .to.be.an('object')
        .with.property('transform');
      expect(yBelow).to.equal(325);
    });

    it('shifts the tooltip offsetX pixels in the x-direction', () => {
      const assertions = [
        {
          params: {
            offsetX: 10
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(260);
          },
        },
        {
          params: {
            offsetX: 0
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(250);
          },
        },
        {
          params: {
            offsetX: -10
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(240);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          Object.assign({}, baseParams, assert.params)
        ));
      });
    });

    it('shifts the tooltip offsetY pixels in the y-direction', () => {
      const assertions = [
        {
          params: {
            offsetY: 10
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(190);
          },
        },
        {
          params: {
            offsetY: 0
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(200);
          },
        },
        {
          params: {
            offsetY: -10
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(310);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          Object.assign({}, baseParams, assert.params)
        ));
      });
    });

    it('guards placement of the tooltip within x-bounds', () => {
      const assertions = [
        {
          params: {
            mouseX: 25,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(25);
          },
        },
        {
          params: {
            mouseX: 25,
            paddingX: 50,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(75);
          },
        },
        {
          params: {
            mouseX: 1175,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(1075);
          },
        },
        {
          params: {
            mouseX: 1000,
            offsetX: 200,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(700);
          },
        },
        {
          params: {
            bounds: {
              x: [400, 700],
            },
            mouseX: 200,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(400);
          },
        },
        {
          params: {
            bounds: {
              x: [400, 700],
            },
            mouseX: 800,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(600);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          merge({}, baseParams, assert.params)
        ));
      });
    });

    it('guards placement of the tooltip within y-bounds', () => {
      const assertions = [
        {
          params: {
            mouseY: 25,
            paddingY: 10,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(35);
          },
        },
        {
          params: {
            mouseY: 25,
            offsetY: 10,
            paddingY: 50,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(65);
          },
        },
        {
          params: {
            mouseY: 550,
            offsetY: -10,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(440);
          },
        },
        {
          params: {
            bounds: {
              y: [200, 400],
            },
            mouseY: 100,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(200);
          },
        },
        {
          params: {
            bounds: {
              y: [200, 400],
            },
            mouseY: 600,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(300);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          merge({}, baseParams, assert.params)
        ));
      });
    });
  });
});
<commit_msg>test that the tooltip only updates its style when necessary<commit_after>/* eslint-disable no-unused-expressions, max-len */
import React from 'react';
import chai, { expect } from 'chai';
import { merge } from 'lodash';
import chaiEnzyme from 'chai-enzyme';
import { mount, shallow } from 'enzyme';

chai.use(chaiEnzyme());

import Tooltip from '../src/tooltip';

function translateVectorFromTransform(transformString) {
  return transformString.match(/(\d+)/g).map(Number);
}

describe('<Tooltip />', () => {
  describe('basic behavior', () => {
    it('does not render if props.show is false', () => {
      const wrapper = shallow(<Tooltip show={false} />);
      expect(wrapper.type()).to.equal(null);
    });

    it('renders if props.show is true', () => {
      const wrapper = shallow(<Tooltip show />);
      expect(wrapper.type()).to.equal('div');
    });

    it('updates position/style if any properties neceessary for calculating position/style is updated', () => {
      const wrapper = mount(
        <Tooltip
          bounds={{ x: [0, 1000], y: [0, 800] }}
          mouseX={5}
          mouseY={5}
          show
        />
      );
      let style = wrapper.state('style');
      Object.entries({
        bounds: {
          x: [200, 300],
          y: [600, 900],
        },
        mouseX: 20,
        mouseY: 30,
        offsetX: 5,
        offsetY: 6,
        paddingX: 20,
        paddingY: 20,
        style: { backgroundColor: 'red' },
      }).forEach(([key, value]) => {
        expect(wrapper.state('style')).to.equal(style);
        wrapper.setProps({
          [key]: value
        });
        const updatedStyle = wrapper.state('style');
        expect(updatedStyle).to.not.equal(style);
        style = updatedStyle;
      });
    });

    it('does not update style if a property not necessary for caclulating styles is updated', () => {
      const wrapper = mount(
        <Tooltip
          bounds={{ x: [0, 1000], y: [0, 800] }}
          mouseX={5}
          mouseY={5}
          show
        />
      );
      const style = wrapper.state('style');
      Object.entries({
        className: 'new-class-name',
        show: false,
      }).forEach(([key, value]) => {
        expect(wrapper.state('style')).to.equal(style);
        wrapper.setProps({
          [key]: value
        });
        const nonUpdatedStyle = wrapper.state('style');
        expect(nonUpdatedStyle).to.equal(style);
      });
    });
  });

  describe('position', () => {
    const baseParams = {
      bounds: {
        x: [0, 1200],
        y: [0, 600],
      },
      height: 100,
      mouseX: 300,
      mouseY: 300,
      offsetX: 0,
      offsetY: 0,
      paddingX: 0,
      paddingY: 0,
      width: 100,
    };

    it('positions the tooltip centered around mouseX', () => {
      const position = Tooltip.getPosition(baseParams);
      const [x] = translateVectorFromTransform(position.transform);
      expect(position)
        .to.be.an('object')
        .with.property('transform');
      expect(x).to.equal(250);
    });

    it('positions the tooltip offsetY above/below from mouseY', () => {
      // above
      const positionAbove = Tooltip.getPosition(Object.assign({}, baseParams, { offsetY: 25 }));
      const [, yAbove] = translateVectorFromTransform(positionAbove.transform);
      expect(positionAbove)
        .to.be.an('object')
        .with.property('transform');
      expect(yAbove).to.equal(175);

      // below
      const positionBelow = Tooltip.getPosition(Object.assign({}, baseParams, { offsetY: -25 }));
      const [, yBelow] = translateVectorFromTransform(positionBelow.transform);
      expect(positionBelow)
        .to.be.an('object')
        .with.property('transform');
      expect(yBelow).to.equal(325);
    });

    it('shifts the tooltip offsetX pixels in the x-direction', () => {
      const assertions = [
        {
          params: {
            offsetX: 10
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(260);
          },
        },
        {
          params: {
            offsetX: 0
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(250);
          },
        },
        {
          params: {
            offsetX: -10
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(240);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          Object.assign({}, baseParams, assert.params)
        ));
      });
    });

    it('shifts the tooltip offsetY pixels in the y-direction', () => {
      const assertions = [
        {
          params: {
            offsetY: 10
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(190);
          },
        },
        {
          params: {
            offsetY: 0
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(200);
          },
        },
        {
          params: {
            offsetY: -10
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(310);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          Object.assign({}, baseParams, assert.params)
        ));
      });
    });

    it('guards placement of the tooltip within x-bounds', () => {
      const assertions = [
        {
          params: {
            mouseX: 25,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(25);
          },
        },
        {
          params: {
            mouseX: 25,
            paddingX: 50,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(75);
          },
        },
        {
          params: {
            mouseX: 1175,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(1075);
          },
        },
        {
          params: {
            mouseX: 1000,
            offsetX: 200,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(700);
          },
        },
        {
          params: {
            bounds: {
              x: [400, 700],
            },
            mouseX: 200,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(400);
          },
        },
        {
          params: {
            bounds: {
              x: [400, 700],
            },
            mouseX: 800,
          },
          expectation: (position) => {
            const [x] = translateVectorFromTransform(position.transform);
            expect(x).to.equal(600);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          merge({}, baseParams, assert.params)
        ));
      });
    });

    it('guards placement of the tooltip within y-bounds', () => {
      const assertions = [
        {
          params: {
            mouseY: 25,
            paddingY: 10,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(35);
          },
        },
        {
          params: {
            mouseY: 25,
            offsetY: 10,
            paddingY: 50,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(65);
          },
        },
        {
          params: {
            mouseY: 550,
            offsetY: -10,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(440);
          },
        },
        {
          params: {
            bounds: {
              y: [200, 400],
            },
            mouseY: 100,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(200);
          },
        },
        {
          params: {
            bounds: {
              y: [200, 400],
            },
            mouseY: 600,
          },
          expectation: (position) => {
            const [, y] = translateVectorFromTransform(position.transform);
            expect(y).to.equal(300);
          },
        }
      ];

      assertions.forEach((assert) => {
        assert.expectation(Tooltip.getPosition(
          merge({}, baseParams, assert.params)
        ));
      });
    });
  });
});
<|endoftext|>


<commit_before>/**
 * @file
 * Control flow for the Kalabox installer.
 */

// Dependencies:
var flow = require('nue').flow,
    as = require('nue').as,
    parallel = require('nue').parallel,
    installUtils = require('./install-utils'),
    exec = require('child_process').exec,
    url = require('url'),
    fs = require('fs'),
    box = require('../box'),
    logger = require('../../logger');

// "Constants":
var VBOX_URL = 'http://files.kalamuna.com/virtualbox-macosx-4.2.8.dmg';
    VBOX_VERSION = '4.2.8',
    TEMP_DIR = '/tmp/',
    VAGRANT_URL = 'http://files.kalamuna.com/vagrant-macosx-1.1.2.dmg',
    VAGRANT_VERSION = '1.1.2',
    KALABOX_DIR = process.env.HOME + '/.kalabox/',
    KALABOX64_URL = 'http://files.kalamuna.com/kalabox64.box',
    KALABOX64_FILENAME = 'kalabox64.box',
    KALASTACK_URL = 'https://codeload.github.com/kalamuna/kalastack/tar.gz/2.x',
    KALASTACK_FILENAME = 'kalastack.tar.gz';

// Installer file info:
var vboxUrlParsed = url.parse(VBOX_URL);
vboxUrlParsed.packageLocation = '/Volumes/VirtualBox/VirtualBox.pkg';
var vagrantUrlParsed = url.parse(VAGRANT_URL);
vagrantUrlParsed.packageLocation = '/Volumes/Vagrant/Vagrant.pkg';

// Helper functions:
function downloadAndReport(url, destination, callback) {
  // Download file via the utils.
  installUtils.downloadFile(url, destination, function(error, percentDone) {
    if (percentDone < 100) {
      // Notify client of progress.
      io.sockets.emit('installer', { complete: percentDone });
    }
    else {
      if (error) {
        installUtils.checkInternet(function(hasInternet) {
          if (!hasInternet) {
            error.message = 'No internet';
            io.sockets.emit('noInternet');
          }
          callback(error);
        });
      }
      else {
        callback(null);
      }
    }
  });
}

function sendMessage(message) {
  io.sockets.emit('installer', { message: message });
}

/**
 * Downloads and installs a DMG file.
 *
 * @param object fileUrl
 *   URL object from url.parse().
 * @param string destination
 *   Directory to store downloaded DMGs, with trailing /.
 * @param string packageLocation
 *   Location of the installer package in the mounted DMG.
 * @param string programName
 *   Name of the software being installed.
 * @param function callback
 *   Callback to invoke on completion.
 */
var installDMG = flow('installDMG')(
  // Begin installation process.
  function installDMG0(fileUrl, destination, packageLocation, programName, callback) {
    this.data.fileUrl = fileUrl;
    this.data.destination = destination;
    this.data.fileName = fileUrl.pathname.split('/').pop();
    this.data.packageLocation = packageLocation;
    this.data.programName = programName;
    this.data.callback = callback;
    // We will be downloading the files to a directory, so make sure it's there
    // This step is not required if you have manually created the directory.
    var mkdir = 'mkdir -p ' + destination;
    var child = exec(mkdir, this.async());
  },
  // Start downloads.
  function installDMG1() {
    // Download vbox.
    sendMessage('Downloading ' + this.data.programName + '...');
    downloadAndReport(this.data.fileUrl.href, this.data.destination, this.async());
  },
  // Confirm download succeeded.
  function installDMG2() {
    fs.exists(this.data.destination + this.data.fileName, this.async(as(0)));
  },
  // Mount disk image.
  function installDMG3(exists) {
    if (!exists) {
      this.endWith({message: 'Software DMG does not exist!'});
      return;
    }
    console.log('Software downloaded!');
    sendMessage('Mounting ' + this.data.programName + ' image...');
    exec('hdiutil attach ' + this.data.destination + this.data.fileName, this.async());
  },
  // Execute installer.
  function installDMG4(stdout, stderr) {
    console.log('DMG mounted.');
    sendMessage('Installing ' + this.data.programName + '...');
    exec('osascript "' + __dirname + '/install_package.scpt" "' + this.data.packageLocation + '" "/"', this.async());
    // @todo Handle user cancelling or failing admin authentication.
  },
  // Unmount DMG after installation.
  function installDMG5(stdout, stderr) {
    console.log('Installed!');
    sendMessage('Installed! Ejecting image...');
    var mountPoint = this.data.packageLocation.split('/');
    mountPoint.pop();
    mountPoint = mountPoint.join('/');
    exec('hdiutil detach ' + mountPoint, this.async());
  },
  function installDMGEnd(stdout, stderr) {
    if (this.err) {
      this.data.callback({message: this.err.message});
      this.err = null;
    }
    this.data.callback();
    this.next();
  }
);

/**
 * Route handler that installs Kalabox.
 */
exports.install = flow('installKalabox')(
  // Check if VBox and Vagrant are installed.
  parallel('installGetVersions')(
    function install0() {
      installUtils.checkVBox(this.async(as(0)));
    },
    function install1() {
      installUtils.checkVagrant(this.async(as(0)));
    }
  ),
  // Check installed versions against required versions.
  function install2(versions) {
    this.data.vboxInstalled = false;
    this.data.vagrantInstalled = false;
    var vboxVersion = versions[0];
    var vagrantVersion = versions[1];
    // Parse and compare VBox version string.
    if (vboxVersion !== false && typeof vboxVersion[0] === 'string') {
      vboxVersion = vboxVersion[0].match(/^(\d+\.\d+\.\d+)r\d*\s*$/);
      if (vboxVersion !== null) {
        vboxVersion = vboxVersion[1];
        // Make sure VBox version is greater than or equal to required.
        if (installUtils.compareVersions(vboxVersion, VBOX_VERSION) >= 0) {
          this.data.vboxInstalled = true;
        }
      }
    }
    // Parse and compare Vagrant version string.
    if (vagrantVersion !== false && typeof vagrantVersion[0] === 'string') {
      vagrantVersion = vagrantVersion[0].match(/^Vagrant version (\d+\.\d+\.\d+)\s*$/);
      if (vagrantVersion !== null) {
        vagrantVersion = vagrantVersion[1];
        // Make sure Vagrant version equals required.
        if (installUtils.compareVersions(vagrantVersion, VAGRANT_VERSION) === 0) {
          this.data.vagrantInstalled = true;
        }
      }
    }
    console.log("VBox installed: " + this.data.vboxInstalled);
    console.log("Vagrant installed: " + this.data.vagrantInstalled);
    this.next();
  },
  // Download and install VBox.
  function install3() {
    if (!this.data.vboxInstalled) {
      installDMG(vboxUrlParsed, TEMP_DIR, vboxUrlParsed.packageLocation, 'VirtualBox', this.async());
    }
    else {
      this.next();
    }
  },
  // Download and install Vagrant.
  function install4() {
    console.log('VirtualBox Installed');
    if (!this.data.vagrantInstalled) {
      installDMG(vagrantUrlParsed, TEMP_DIR, vagrantUrlParsed.packageLocation, 'Vagrant', this.async());
    }
    else {
      this.next();
    }
  },
  // @todo Verify that VBox and Vagrant were installed successfully.
  // Create the .kalabox directory in home.
  function install5() {
    console.log('Vagrant installed.');
    exec('mkdir -p "' + KALABOX_DIR + '"', this.async());
  },
  // Download Kalabox image.
  function install6(stdout, stderr) {
    sendMessage('Downloading the Kalabox Ubuntu image...');
    downloadAndReport(KALABOX64_URL, KALABOX_DIR, this.async());
  },
  // Verify Kalabox was downloaded.
  function install7() {
    fs.exists(KALABOX_DIR + KALABOX64_FILENAME, this.async(as(0)));
  },
  // Download Kalastack archive from GitHub if download made it.
  function install8(exists) {
    if (!exists) {
      this.endWith({message: 'Failed to download Kalabox image.'});
      return;
    }
    console.log('Downloaded Kalabox image.');
    sendMessage('Downloading Kalastack...');
    exec('curl -L -o ' + KALASTACK_FILENAME + ' ' + KALASTACK_URL, {cwd: KALABOX_DIR}, this.async());
  },
  // Verify Kalastack archive was downloaded.
  function install9(stdout, stderr) {
    fs.exists(KALABOX_DIR + KALASTACK_FILENAME, this.async(as(0)));
  },
  // Extract Kalastack from downloaded archive if download succeeded.
  function install10(exists) {
    if (!exists) {
      this.endWith({message: 'Failed to download Kalastack archive.'});
      return;
    }
    exec('tar zxvf ' + KALASTACK_FILENAME, {cwd: KALABOX_DIR}, this.async());
  },
  // @todo Delete Kalastack tar.gz file.
  // Add Kalabox hosts config to /etc/hosts.
  function install11(stdout, stderr) {
    exec('osascript "' + __dirname + '/edit_hosts.scpt" "' + __dirname + '/hosts_config"', this.async());
  },
  // Start box build from Kalabox image.
  function install12(stdout, stderr) {
    console.log('Extracted Kalastack...');
    sendMessage('Building the box...');
    exec('vagrant box add kalabox "' + KALABOX_DIR + KALABOX64_FILENAME + '"', {cwd: KALABOX_DIR + 'kalastack-2.x'}, this.async());
  },
  // Finish box build with "vagrant up".
  function install13(stdout, stderr) {
    console.log('Kalabox added');
    exec('osascript "' + __dirname + '/spinup_box.scpt" "' + KALABOX_DIR + '/kalastack-2.x"', this.async());
    // Temporary measure to prevent installer from continuing until "vagrant up" completes in new Terminal.
    var nextStep = this.async();
    var moveOn = function() {
      if (box.isInstalled()) {
        clearInterval(interval);
        nextStep();
      }
    };
    var interval = setInterval(function() {
      box.initialize(moveOn);
    }, 20000);
  },
  function installEnd(stdout, stderr) {
    if (this.err) {
      if (this.err.message == 'No internet') {
        logger.error('Error during installation routine: ' + this.err.message);
      }
      this.err = null;
      this.next();
    }
    else {
      console.log('Box built!');
      io.sockets.emit('installerComplete');
      this.next();
    }
  }
);
<commit_msg>Delete kalastack tarball after install.<commit_after>/**
 * @file
 * Control flow for the Kalabox installer.
 */

// Dependencies:
var flow = require('nue').flow,
    as = require('nue').as,
    parallel = require('nue').parallel,
    installUtils = require('./install-utils'),
    exec = require('child_process').exec,
    url = require('url'),
    fs = require('fs'),
    box = require('../box'),
    logger = require('../../logger');

// "Constants":
var VBOX_URL = 'http://files.kalamuna.com/virtualbox-macosx-4.2.8.dmg';
    VBOX_VERSION = '4.2.8',
    TEMP_DIR = '/tmp/',
    VAGRANT_URL = 'http://files.kalamuna.com/vagrant-macosx-1.1.2.dmg',
    VAGRANT_VERSION = '1.1.2',
    KALABOX_DIR = process.env.HOME + '/.kalabox/',
    KALABOX64_URL = 'http://files.kalamuna.com/kalabox64.box',
    KALABOX64_FILENAME = 'kalabox64.box',
    KALASTACK_URL = 'https://codeload.github.com/kalamuna/kalastack/tar.gz/2.x',
    KALASTACK_FILENAME = 'kalastack.tar.gz';

// Installer file info:
var vboxUrlParsed = url.parse(VBOX_URL);
vboxUrlParsed.packageLocation = '/Volumes/VirtualBox/VirtualBox.pkg';
var vagrantUrlParsed = url.parse(VAGRANT_URL);
vagrantUrlParsed.packageLocation = '/Volumes/Vagrant/Vagrant.pkg';

// Helper functions:
function downloadAndReport(url, destination, callback) {
  // Download file via the utils.
  installUtils.downloadFile(url, destination, function(error, percentDone) {
    if (percentDone < 100) {
      // Notify client of progress.
      io.sockets.emit('installer', { complete: percentDone });
    }
    else {
      if (error) {
        installUtils.checkInternet(function(hasInternet) {
          if (!hasInternet) {
            error.message = 'No internet';
            io.sockets.emit('noInternet');
          }
          callback(error);
        });
      }
      else {
        callback(null);
      }
    }
  });
}

function sendMessage(message) {
  io.sockets.emit('installer', { message: message });
}

/**
 * Downloads and installs a DMG file.
 *
 * @param object fileUrl
 *   URL object from url.parse().
 * @param string destination
 *   Directory to store downloaded DMGs, with trailing /.
 * @param string packageLocation
 *   Location of the installer package in the mounted DMG.
 * @param string programName
 *   Name of the software being installed.
 * @param function callback
 *   Callback to invoke on completion.
 */
var installDMG = flow('installDMG')(
  // Begin installation process.
  function installDMG0(fileUrl, destination, packageLocation, programName, callback) {
    this.data.fileUrl = fileUrl;
    this.data.destination = destination;
    this.data.fileName = fileUrl.pathname.split('/').pop();
    this.data.packageLocation = packageLocation;
    this.data.programName = programName;
    this.data.callback = callback;
    // We will be downloading the files to a directory, so make sure it's there
    // This step is not required if you have manually created the directory.
    var mkdir = 'mkdir -p ' + destination;
    var child = exec(mkdir, this.async());
  },
  // Start downloads.
  function installDMG1() {
    // Download vbox.
    sendMessage('Downloading ' + this.data.programName + '...');
    downloadAndReport(this.data.fileUrl.href, this.data.destination, this.async());
  },
  // Confirm download succeeded.
  function installDMG2() {
    fs.exists(this.data.destination + this.data.fileName, this.async(as(0)));
  },
  // Mount disk image.
  function installDMG3(exists) {
    if (!exists) {
      this.endWith({message: 'Software DMG does not exist!'});
      return;
    }
    console.log('Software downloaded!');
    sendMessage('Mounting ' + this.data.programName + ' image...');
    exec('hdiutil attach ' + this.data.destination + this.data.fileName, this.async());
  },
  // Execute installer.
  function installDMG4(stdout, stderr) {
    console.log('DMG mounted.');
    sendMessage('Installing ' + this.data.programName + '...');
    exec('osascript "' + __dirname + '/install_package.scpt" "' + this.data.packageLocation + '" "/"', this.async());
    // @todo Handle user cancelling or failing admin authentication.
  },
  // Unmount DMG after installation.
  function installDMG5(stdout, stderr) {
    console.log('Installed!');
    sendMessage('Installed! Ejecting image...');
    var mountPoint = this.data.packageLocation.split('/');
    mountPoint.pop();
    mountPoint = mountPoint.join('/');
    exec('hdiutil detach ' + mountPoint, this.async());
  },
  function installDMGEnd(stdout, stderr) {
    if (this.err) {
      this.data.callback({message: this.err.message});
      this.err = null;
    }
    this.data.callback();
    this.next();
  }
);

/**
 * Route handler that installs Kalabox.
 */
exports.install = flow('installKalabox')(
  // Check if VBox and Vagrant are installed.
  parallel('installGetVersions')(
    function install0() {
      installUtils.checkVBox(this.async(as(0)));
    },
    function install1() {
      installUtils.checkVagrant(this.async(as(0)));
    }
  ),
  // Check installed versions against required versions.
  function install2(versions) {
    this.data.vboxInstalled = false;
    this.data.vagrantInstalled = false;
    var vboxVersion = versions[0];
    var vagrantVersion = versions[1];
    // Parse and compare VBox version string.
    if (vboxVersion !== false && typeof vboxVersion[0] === 'string') {
      vboxVersion = vboxVersion[0].match(/^(\d+\.\d+\.\d+)r\d*\s*$/);
      if (vboxVersion !== null) {
        vboxVersion = vboxVersion[1];
        // Make sure VBox version is greater than or equal to required.
        if (installUtils.compareVersions(vboxVersion, VBOX_VERSION) >= 0) {
          this.data.vboxInstalled = true;
        }
      }
    }
    // Parse and compare Vagrant version string.
    if (vagrantVersion !== false && typeof vagrantVersion[0] === 'string') {
      vagrantVersion = vagrantVersion[0].match(/^Vagrant version (\d+\.\d+\.\d+)\s*$/);
      if (vagrantVersion !== null) {
        vagrantVersion = vagrantVersion[1];
        // Make sure Vagrant version equals required.
        if (installUtils.compareVersions(vagrantVersion, VAGRANT_VERSION) === 0) {
          this.data.vagrantInstalled = true;
        }
      }
    }
    console.log("VBox installed: " + this.data.vboxInstalled);
    console.log("Vagrant installed: " + this.data.vagrantInstalled);
    this.next();
  },
  // Download and install VBox.
  function install3() {
    if (!this.data.vboxInstalled) {
      installDMG(vboxUrlParsed, TEMP_DIR, vboxUrlParsed.packageLocation, 'VirtualBox', this.async());
    }
    else {
      this.next();
    }
  },
  // Download and install Vagrant.
  function install4() {
    console.log('VirtualBox Installed');
    if (!this.data.vagrantInstalled) {
      installDMG(vagrantUrlParsed, TEMP_DIR, vagrantUrlParsed.packageLocation, 'Vagrant', this.async());
    }
    else {
      this.next();
    }
  },
  // @todo Verify that VBox and Vagrant were installed successfully.
  // Create the .kalabox directory in home.
  function install5() {
    console.log('Vagrant installed.');
    exec('mkdir -p "' + KALABOX_DIR + '"', this.async());
  },
  // Download Kalabox image.
  function install6(stdout, stderr) {
    sendMessage('Downloading the Kalabox Ubuntu image...');
    downloadAndReport(KALABOX64_URL, KALABOX_DIR, this.async());
  },
  // Verify Kalabox was downloaded.
  function install7() {
    fs.exists(KALABOX_DIR + KALABOX64_FILENAME, this.async(as(0)));
  },
  // Download Kalastack archive from GitHub if download made it.
  function install8(exists) {
    if (!exists) {
      this.endWith({message: 'Failed to download Kalabox image.'});
      return;
    }
    console.log('Downloaded Kalabox image.');
    sendMessage('Downloading Kalastack...');
    exec('curl -L -o ' + KALASTACK_FILENAME + ' ' + KALASTACK_URL, {cwd: KALABOX_DIR}, this.async());
  },
  // Verify Kalastack archive was downloaded.
  function install9(stdout, stderr) {
    fs.exists(KALABOX_DIR + KALASTACK_FILENAME, this.async(as(0)));
  },
  // Extract Kalastack from downloaded archive if download succeeded.
  function install10(exists) {
    if (!exists) {
      this.endWith({message: 'Failed to download Kalastack archive.'});
      return;
    }
    exec('tar zxvf ' + KALASTACK_FILENAME, {cwd: KALABOX_DIR}, this.async());
  },
  // Delete Kalastack tar.gz file.
  function install11(stdout, stderr) {
    exec('rm ' + KALASTACK_FILENAME, {cwd: KALABOX_DIR}, this.async());
  },
  // Add Kalabox hosts config to /etc/hosts.
  function install12(stdout, stderr) {
    exec('osascript "' + __dirname + '/edit_hosts.scpt" "' + __dirname + '/hosts_config"', this.async());
  },
  // Start box build from Kalabox image.
  function install13(stdout, stderr) {
    console.log('Extracted Kalastack...');
    sendMessage('Building the box...');
    exec('vagrant box add kalabox "' + KALABOX_DIR + KALABOX64_FILENAME + '"', {cwd: KALABOX_DIR + 'kalastack-2.x'}, this.async());
  },
  // Finish box build with "vagrant up".
  function install14(stdout, stderr) {
    console.log('Kalabox added');
    exec('osascript "' + __dirname + '/spinup_box.scpt" "' + KALABOX_DIR + '/kalastack-2.x"', this.async());
    // Temporary measure to prevent installer from continuing until "vagrant up" completes in new Terminal.
    var nextStep = this.async();
    var moveOn = function() {
      if (box.isInstalled()) {
        clearInterval(interval);
        nextStep();
      }
    };
    var interval = setInterval(function() {
      box.initialize(moveOn);
    }, 20000);
  },
  function installEnd(stdout, stderr) {
    if (this.err) {
      if (this.err.message == 'No internet') {
        logger.error('Error during installation routine: ' + this.err.message);
      }
      this.err = null;
      this.next();
    }
    else {
      console.log('Box built!');
      io.sockets.emit('installerComplete');
      this.next();
    }
  }
);
<|endoftext|>


<commit_before>import verify            from '../verify';
import isString          from 'lodash.isstring';
import isFunction        from 'lodash.isfunction';
import {isValidRoute}    from './createRoute';
import shapedReducer     from './shapedReducer';

/**
 * @function createFeature
 * @description
 *
 * Create a new Feature object, that accumulates various feature
 * aspects to be consumed by feature-u runApp().
 *
 * Example:
 * ```js
 *   import {createFeature} from 'feature-u';
 *   import reducer         from './state';
 *   export default createFeature({
 *     name:       'views',
 *     enabled:    true,
 *     reducer:    shapedReducer('views.currentView', reducer),
 *     ? more
 *   };
 * ```
 *
 * **Please Note** `createFeature()` accepts named parameters.
 *
 * @param {string} name the feature name, used in
 * programmatically delineating various features (ex: 'views').
 *
 * @param {boolean} [enabled=true] an indicator as to
 * whether this feature is enabled (true) or not (false).
 *
 * @param {Any|contextCallback} [publicAPI] an optional
 * resource exposed in app.{featureName}.{publicAPI} (emitted from
 * runApp()), promoting cross-communication between features.  Please
 * refer to the feature-u `Public API` documentation for more
 * detail.
 *
 * Because some publicAPI may require feature-based context
 * information, this parameter can also be a contextCallback - a
 * function that returns the publicAPI (see injectContext()).
 *
 * @param {reducerFn|contextCallback} [reducer] an optional
 * reducer that maintains redux state (if any) for this feature.
 * feature-u patches each reducer into the overall app state, by
 * default using the `feature.name`, but can be explicitly defined
 * through the shapedReducer() (embellishing the reducer with a shape
 * specification).  Please refer to the feature-u `Reducers`
 * documentation for more detail.
 *
 * Because some reducers may require feature-based context
 * information, this parameter can also be a contextCallback - a
 * function that returns the reducerFn (see injectContext()).
 *
 * @param {Logic[]|contextCallback} [logic] an optional set
 * of business logic modules (if any) to be registered to redux-logic
 * in support of this feature. Please refer to the feature-u `Logic`
 * documentation for more detail.
 *
 * Because some logic modules may require feature-based context
 * information, this parameter can also be a contextCallback - a
 * function that returns the Logic[] (see injectContext()).
 *
 * @param {Route} [route] the optional route callback (see
 * createRoute()) that promotes feature-based top-level screen
 * components based on appState.  Please refer to the feature-u
 * `routes` documentation for more detail.
 *
 * @param {function} [appWillStart] an optional app life-cycle hook
 * invoked one-time at app startup time.  `API: appWillStart(app,
 * children): optional-top-level-content` This life-cycle hook can do
 * any type of initialization, and/or optionally supplement the app's
 * top-level content (using a non-null return).  Please refer to the
 * feature-u `App Life Cycle Hooks` documentation for more detail.
 *
 * @param {function} [appDidStart] an optional app life-cycle hook
 * invoked one-time immediately after app has started.  `API:
 * appDidStart({app, appState, dispatch}): void` Because the app is
 * up-and-running at this time, you have access to the appState and
 * the dispatch function.  Please refer to the feature-u `App Life
 * Cycle Hooks` documentation for more detail.
 *
 * @return {Feature} a new Feature object (to be consumed by feature-u runApp()).
 */
export default function createFeature({name,
                                       enabled=true,

                                       publicAPI,

                                       reducer,
                                       logic,
                                       route,

                                       appWillStart,
                                       appDidStart,
                                       ...unknownArgs}={}) {

  // ***
  // *** validate parameters
  // ***

  const check = verify.prefix('createFeature() parameter violation: ');

  check(name,            'name is required');
  check(isString(name),  'name must be a string');

  check(enabled===true||enabled===false, 'enabled must be a boolean');

  // publicAPI: nothing to validate (it can be anything, INCLUDING a .injectContext function)

  if (reducer) {
    check(isFunction(reducer) || reducer.injectContext, 'reducer (when supplied) must be a function -or- a contextCallback');

    // default reducer shape to our feature name
    if (!reducer.shape) {
      shapedReducer(name, reducer);
    }
  }

  if (logic) {
    check(Array.isArray(logic) || logic.injectContext, 'logic (when supplied) must be an array of redux-logic modules -or- a contextCallback');
  }

  if (route) {
    const routeMsg = isValidRoute(route);
    check(!routeMsg, routeMsg);
  }

  if (appWillStart) {
    check(isFunction(appWillStart), 'appWillStart (when supplied) must be a function');
  }

  if (appDidStart) {
    check(isFunction(appDidStart), 'appDidStart (when supplied) must be a function');
  }

  const unknownArgKeys = Object.keys(unknownArgs);
  check(unknownArgKeys.length===0,  `unrecognized named parameter(s): ${unknownArgKeys}`);

  // ***
  // *** return a new Feature object, accumulating feature aspects
  // ***

  // NOTES:
  //  *P*: we pre-register all "raw" feature aspects
  //  *E*: some of which may NOT yet be fully expanded
  //       ... ones that support (and use) the injectContext() callback wrapper
  //       ... this expansion is controlled by runApp()
  //           to insure the publicAPI of ALL features are expanded FIRST,
  //           for other feature aspect expansion to use
  //  *D*: some aspects contain "digestible" info that can be used internally in
  //       aspect expansion (for single-source-of-truth)
  return {
    name,          // *P* *D*
    enabled,       // *P* *D*

    publicAPI,     // *P* *D* *E* KEY: this aspect is the feature's public API

    reducer,       // *P* *D* *E* NOTE: digestible info for reducers (*D*) are simply reducer.getShapedState(appState)
    logic,         // *P*     *E*
    route,         // *P*

    appWillStart,  // *P*
    appDidStart,   // *P*
  };
}

/**
 * @private
 *
 * Expand the publicAPI aspect of the supplied feature, when it is
 * employing the injectContext() callback wrapper (see *E* above).
 *
 * This is invoked by runApp() to insure the publicAPI of ALL features
 * are expanded FIRST, so that other feature aspect expansion can use
 * it.
 *
 * This eliminates order dependency issues related to feature
 * expansion - EVEN in code that is expanded in-line.  The only
 * exception to this is dependencies in the publicAPI itself (which
 * should be an anti-pattern).
 *
 * @param {Feature} feature the Feature object for which to expand the
 * publicAPI.
 *
 * @param {App} app the App object (emitted by runApp()).
 */
export function expandFeatureAspect_publicAPI(feature, app) {
  if (feature.publicAPI && feature.publicAPI.injectContext) {
    feature.publicAPI = feature.publicAPI(feature, app);
  }
}

/**
 * @private
 *
 * Expand all other aspects of the supplied feature, when they are
 * employing the injectContext() callback wrapper (see *E* above).
 *
 * This is invoked by runApp() to insure the publicAPI of ALL features
 * are expanded BEFORE all other aspects, so that they can use the
 * publicAPI.
 *
 * This eliminates order dependency issues related to feature
 * expansion - EVEN in code that is expanded in-line.  The only
 * exception to this is dependencies in the publicAPI itself (which
 * should be an anti-pattern).
 *
 * @param {Feature} feature the Feature object for which to expand the
 * aspects.
 *
 * @param {App} app the App object (emitted by runApp()).
 */
export function expandFeatureAspects(feature, app) {

  // TODO: #reducerPartOfOtherFeature: here we expand the reducers
  //       - can we somehow do more?
  //       - like pass in un-attached reducers,
  //       - giving other features the oppertunity to include them?
  // ... reducer
  if (feature.reducer && feature.reducer.injectContext) {

    // hold on to our reducer shape
    // ... we know shape is available, because WE default it (above)
    const shape = feature.reducer.shape;

    // fully resolve our actual reducer
    feature.reducer = feature.reducer(feature, app);

    // validate that no incompatable shape has been defined within our resolved reducer
    if (feature.reducer.shape) {
      verify(feature.reducer.shape === shape, `createFeature() parameter violation: reducer contextCallback shape: '${shape}' is different from resolved reducer shape: '${feature.reducer.shape}'.
SideBar: When BOTH shapedReducer() and injectContext() are needed, shapedReducer() should be adorned ONLY in the outer function passed to createFunction().`);
    }

    // apply same shape to our final resolved reducer
    // ... so feature.reducer.getShapedState(appState) is available for public access
    shapedReducer(feature.reducer, shape);
  }

  // ... logic
  if (feature.logic && feature.logic.injectContext) {
    feature.logic = feature.logic(feature, app);
  }

}
<commit_msg>fixed bug in parameter order to shapedReducer()<commit_after>import verify            from '../verify';
import isString          from 'lodash.isstring';
import isFunction        from 'lodash.isfunction';
import {isValidRoute}    from './createRoute';
import shapedReducer     from './shapedReducer';

/**
 * @function createFeature
 * @description
 *
 * Create a new Feature object, that accumulates various feature
 * aspects to be consumed by feature-u runApp().
 *
 * Example:
 * ```js
 *   import {createFeature} from 'feature-u';
 *   import reducer         from './state';
 *   export default createFeature({
 *     name:       'views',
 *     enabled:    true,
 *     reducer:    shapedReducer('views.currentView', reducer),
 *     ? more
 *   };
 * ```
 *
 * **Please Note** `createFeature()` accepts named parameters.
 *
 * @param {string} name the feature name, used in
 * programmatically delineating various features (ex: 'views').
 *
 * @param {boolean} [enabled=true] an indicator as to
 * whether this feature is enabled (true) or not (false).
 *
 * @param {Any|contextCallback} [publicAPI] an optional
 * resource exposed in app.{featureName}.{publicAPI} (emitted from
 * runApp()), promoting cross-communication between features.  Please
 * refer to the feature-u `Public API` documentation for more
 * detail.
 *
 * Because some publicAPI may require feature-based context
 * information, this parameter can also be a contextCallback - a
 * function that returns the publicAPI (see injectContext()).
 *
 * @param {reducerFn|contextCallback} [reducer] an optional
 * reducer that maintains redux state (if any) for this feature.
 * feature-u patches each reducer into the overall app state, by
 * default using the `feature.name`, but can be explicitly defined
 * through the shapedReducer() (embellishing the reducer with a shape
 * specification).  Please refer to the feature-u `Reducers`
 * documentation for more detail.
 *
 * Because some reducers may require feature-based context
 * information, this parameter can also be a contextCallback - a
 * function that returns the reducerFn (see injectContext()).
 *
 * @param {Logic[]|contextCallback} [logic] an optional set
 * of business logic modules (if any) to be registered to redux-logic
 * in support of this feature. Please refer to the feature-u `Logic`
 * documentation for more detail.
 *
 * Because some logic modules may require feature-based context
 * information, this parameter can also be a contextCallback - a
 * function that returns the Logic[] (see injectContext()).
 *
 * @param {Route} [route] the optional route callback (see
 * createRoute()) that promotes feature-based top-level screen
 * components based on appState.  Please refer to the feature-u
 * `routes` documentation for more detail.
 *
 * @param {function} [appWillStart] an optional app life-cycle hook
 * invoked one-time at app startup time.  `API: appWillStart(app,
 * children): optional-top-level-content` This life-cycle hook can do
 * any type of initialization, and/or optionally supplement the app's
 * top-level content (using a non-null return).  Please refer to the
 * feature-u `App Life Cycle Hooks` documentation for more detail.
 *
 * @param {function} [appDidStart] an optional app life-cycle hook
 * invoked one-time immediately after app has started.  `API:
 * appDidStart({app, appState, dispatch}): void` Because the app is
 * up-and-running at this time, you have access to the appState and
 * the dispatch function.  Please refer to the feature-u `App Life
 * Cycle Hooks` documentation for more detail.
 *
 * @return {Feature} a new Feature object (to be consumed by feature-u runApp()).
 */
export default function createFeature({name,
                                       enabled=true,

                                       publicAPI,

                                       reducer,
                                       logic,
                                       route,

                                       appWillStart,
                                       appDidStart,
                                       ...unknownArgs}={}) {

  // ***
  // *** validate parameters
  // ***

  const check = verify.prefix('createFeature() parameter violation: ');

  check(name,            'name is required');
  check(isString(name),  'name must be a string');

  check(enabled===true||enabled===false, 'enabled must be a boolean');

  // publicAPI: nothing to validate (it can be anything, INCLUDING a .injectContext function)

  if (reducer) {
    check(isFunction(reducer) || reducer.injectContext, 'reducer (when supplied) must be a function -or- a contextCallback');

    // default reducer shape to our feature name
    if (!reducer.shape) {
      shapedReducer(name, reducer);
    }
  }

  if (logic) {
    check(Array.isArray(logic) || logic.injectContext, 'logic (when supplied) must be an array of redux-logic modules -or- a contextCallback');
  }

  if (route) {
    const routeMsg = isValidRoute(route);
    check(!routeMsg, routeMsg);
  }

  if (appWillStart) {
    check(isFunction(appWillStart), 'appWillStart (when supplied) must be a function');
  }

  if (appDidStart) {
    check(isFunction(appDidStart), 'appDidStart (when supplied) must be a function');
  }

  const unknownArgKeys = Object.keys(unknownArgs);
  check(unknownArgKeys.length===0,  `unrecognized named parameter(s): ${unknownArgKeys}`);

  // ***
  // *** return a new Feature object, accumulating feature aspects
  // ***

  // NOTES:
  //  *P*: we pre-register all "raw" feature aspects
  //  *E*: some of which may NOT yet be fully expanded
  //       ... ones that support (and use) the injectContext() callback wrapper
  //       ... this expansion is controlled by runApp()
  //           to insure the publicAPI of ALL features are expanded FIRST,
  //           for other feature aspect expansion to use
  //  *D*: some aspects contain "digestible" info that can be used internally in
  //       aspect expansion (for single-source-of-truth)
  return {
    name,          // *P* *D*
    enabled,       // *P* *D*

    publicAPI,     // *P* *D* *E* KEY: this aspect is the feature's public API

    reducer,       // *P* *D* *E* NOTE: digestible info for reducers (*D*) are simply reducer.getShapedState(appState)
    logic,         // *P*     *E*
    route,         // *P*

    appWillStart,  // *P*
    appDidStart,   // *P*
  };
}

/**
 * @private
 *
 * Expand the publicAPI aspect of the supplied feature, when it is
 * employing the injectContext() callback wrapper (see *E* above).
 *
 * This is invoked by runApp() to insure the publicAPI of ALL features
 * are expanded FIRST, so that other feature aspect expansion can use
 * it.
 *
 * This eliminates order dependency issues related to feature
 * expansion - EVEN in code that is expanded in-line.  The only
 * exception to this is dependencies in the publicAPI itself (which
 * should be an anti-pattern).
 *
 * @param {Feature} feature the Feature object for which to expand the
 * publicAPI.
 *
 * @param {App} app the App object (emitted by runApp()).
 */
export function expandFeatureAspect_publicAPI(feature, app) {
  if (feature.publicAPI && feature.publicAPI.injectContext) {
    feature.publicAPI = feature.publicAPI(feature, app);
  }
}

/**
 * @private
 *
 * Expand all other aspects of the supplied feature, when they are
 * employing the injectContext() callback wrapper (see *E* above).
 *
 * This is invoked by runApp() to insure the publicAPI of ALL features
 * are expanded BEFORE all other aspects, so that they can use the
 * publicAPI.
 *
 * This eliminates order dependency issues related to feature
 * expansion - EVEN in code that is expanded in-line.  The only
 * exception to this is dependencies in the publicAPI itself (which
 * should be an anti-pattern).
 *
 * @param {Feature} feature the Feature object for which to expand the
 * aspects.
 *
 * @param {App} app the App object (emitted by runApp()).
 */
export function expandFeatureAspects(feature, app) {

  // TODO: #reducerPartOfOtherFeature: here we expand the reducers
  //       - can we somehow do more?
  //       - like pass in un-attached reducers,
  //       - giving other features the oppertunity to include them?
  // ... reducer
  if (feature.reducer && feature.reducer.injectContext) {

    // hold on to our reducer shape
    // ... we know shape is available, because WE default it (above)
    const shape = feature.reducer.shape;

    // fully resolve our actual reducer
    feature.reducer = feature.reducer(feature, app);

    // validate that no incompatable shape has been defined within our resolved reducer
    if (feature.reducer.shape) {
      verify(feature.reducer.shape === shape, `createFeature() parameter violation: reducer contextCallback shape: '${shape}' is different from resolved reducer shape: '${feature.reducer.shape}'.
SideBar: When BOTH shapedReducer() and injectContext() are needed, shapedReducer() should be adorned ONLY in the outer function passed to createFunction().`);
    }

    // apply same shape to our final resolved reducer
    // ... so feature.reducer.getShapedState(appState) is available for public access
    shapedReducer(shape, feature.reducer);
  }

  // ... logic
  if (feature.logic && feature.logic.injectContext) {
    feature.logic = feature.logic(feature, app);
  }

}
<|endoftext|>


<commit_before>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF 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.
 */

/*global $, Config */

var sakai = sakai || {};

/**
 * @name sakai.creategroup
 *
 * @class creategroup
 *
 * @description
 * Creategroup widget
 *
 * @version 0.0.1
 * @param {String} tuid Unique id of the widget
 * @param {Boolean} showSettings Show the settings of the widget or not
 */
sakai.creategroup = function(tuid, showSettings){

    /////////////////////////////
    // Configuration variables //
    /////////////////////////////

    // - ID
    var createGroup = "#creategroup";

    // Container
    var createGroupContainer = createGroup + "_container";

    // template containers
    var createGroupAddTemplate = "#noncourse_template_container";

    // Non course
    var createGroupAdd = createGroup + "_add";
    var createGroupAddCancel = createGroupAdd + "_cancel";
    var createGroupAddDescription = createGroupAdd + "_description";
    var createGroupAddId = createGroupAdd + "_id";
    var createGroupAddName = createGroupAdd + "_name";
    var createGroupAddProcess = createGroupAdd + "_process";
    var createGroupAddSave = createGroupAdd + "_save";
    var createGroupAddUrl = createGroupAdd + "_url";

    // Error fields
    var createGroupAddNameEmpty = createGroupAddName + "_empty";
    var createGroupAddNameShort = createGroupAddName + "_short";
    var createGroupAddIdEmpty = createGroupAddId + "_empty";
    var createGroupAddIdTaken = createGroupAddId + "_taken";
    var createGroupAddIdShort = createGroupAddId + "_short";
    var errorFields = ".creategroup_error_msg";

    // CSS Classes
    var invalidFieldClass = "invalid";

    ///////////////////////
    // Utility functions //
    ///////////////////////

    /**
     * Public function that can be called from elsewhere
     * (e.g. chat and sites widget)
     * It initializes the creategroup widget and shows the jqmodal (ligthbox)
     */
    sakai.creategroup.initialise = function(){
        $(".description_fields").show();
        $("#creategroup_add_container").show();
        $(createGroupAddTemplate).show();
        $(createGroupAddName).val("");
        $(createGroupAddDescription).val("");
        $(createGroupAddId).val("");
        $(createGroupContainer).jqmShow();
    };

    /**
     * Show or hide the process div and hide/shows the buttons
     * @param {Boolean} show
     *     true: show the process div and hide the buttons
     *     false: hide the process div and show the buttons
     */
    var showProcess = function(show){
        if(show){
            $(createGroupAddCancel).hide();
            $(createGroupAddSave).hide();
            $(createGroupAddProcess).show();
        }else{
            $(createGroupAddProcess).hide();
            $(createGroupAddCancel).show();
            $(createGroupAddSave).show();
        }
    };

    /**
     * Replace or remove malicious characters from the string
     * We use this function to modify the groupid
     * String to test against: test :?=&;\/?@+$<>#%'"''{}|\\^[]'
     * @param {Object} input The string where the characters need to be replaced
     */
    var replaceCharacters = function(input){

        input = $.trim(input); // Remove the spaces at the beginning and end of the id

        input = input.toLowerCase().replace(/ /g,"-");
        input = input.toLowerCase().replace(/'/g,"");
        input = input.toLowerCase().replace(/"/g,"");

        var regexp = new RegExp("[^a-z0-9_-]", "gi");
        input = input.replace(regexp,"_");

        return input;
    };

    ////////////////////
    // Error handling //
    ////////////////////

    var resetErrorFields = function(){
        $("input").removeClass(invalidFieldClass);
        $("textarea").removeClass(invalidFieldClass);
        $(errorFields).hide();
    };

    /**
     * Function that will visually mark a form field as an
     * invalid field.
     * @param String field
     *  JQuery selector of the input box we want to show as invalid
     * @param String errorField
     *  JQuery selector of the error message that needs to be shown.
     * @param boolean noReset
     *  Parameter that specifies whether we need to make all of the
     *  fiels valid again first
     */
    var setError = function(field,errorField, noReset){
        if (!noReset) {
            resetErrorFields();
        }
        $(field).addClass(invalidFieldClass);
        $(errorField).show();
    };

    var myClose = function(hash) {
        resetErrorFields();
        hash.o.remove();
        hash.w.hide();
    };

    ///////////////////
    // Create a group //
    ///////////////////

    /**
     * Check if the group is created correctly and exists
     * @param {String} groupid
     */
    var doCheckGroup = function(groupid){
        // Check if the group exists.
        var groupExists = false;

        $.ajax({
            url: "/~" + groupid + ".json",
            type: "GET",
            async: false,
            success: function(data, textStatus){
                groupExists = true;
            }
        });
        return groupExists;
    };

    /**
     * Create the group.
     * @param {String} groupid the id of the group that's being created
     * @param {String} grouptitle the title of the group that's being created
     * @param {String} groupdescription the description of the group that's being created
     * @param {String} groupidManagers the id of the managers group for the group that's being created
    */
    var doSaveGroup = function(groupid, grouptitle, groupdescription, groupidManagers){
    // Create a group with the managers group

        $.ajax({
            url: sakai.config.URL.GROUP_CREATE_SERVICE,
            data: {
                "_charset_":"utf-8",
                ":name": groupid,
                ":manager": groupidManagers,
                "title" : grouptitle,
                "description" : groupdescription
            },
            type: "POST",
            success: function(data, textStatus){
                //check if the group exists
                if (doCheckGroup(groupid)) {
                    //redirect the user to the group
                    document.location = "/dev/group.html?id=" + groupid;
                }
            },
            error: function(xhr, textStatus, thrownError){
                var groupCheck = doCheckGroup(groupid);
                if (groupCheck){
                    setError(createGroupAddId,createGroupAddIdTaken,true);
                } else {
                    fluid.log("An error has occurred: " + xhr.status + " " + xhr.statusText);
                }
                showProcess(false);
            }
        });
    };

    /**
     * Create the managers group.
     * @param {String} groupid the id of the group that's being created
     * @param {String} grouptitle the title of the group that's being created
     * @param {String} groupdescription the description of the group that's being created
    */
    var doSaveGroupManagers = function(groupid, grouptitle, groupdescription){
    // Create the groups
        var groupidManagers = groupid + "-managers";
        var grouptitleManagers = grouptitle + " Managers";

        $.ajax({
            url: sakai.config.URL.GROUP_CREATE_SERVICE,
            data: {
                "_charset_":"utf-8",
                ":name": groupidManagers,
                ":member": sakai.data.me.user.userid,
                "title" : grouptitleManagers,
                "description" : groupdescription
            },
            type: "POST",
            success: function(data, textStatus){
                // check if the managers group exists proceed
                if (doCheckGroup(groupidManagers)) {
                    // create the actual group
                    doSaveGroup(groupid, grouptitle, groupdescription, groupidManagers);
                }
            },
            error: function(xhr, textStatus, thrownError){
                var groupCheck = doCheckGroup(groupid);
                if (groupCheck){
                    setError(createGroupAddId,createGroupAddIdTaken,true);
                } else {
                    fluid.log("An error has occurred: " + xhr.status + " " + xhr.statusText);
                }
                showProcess(false);
            }
        });
    };

    var saveGroup = function(){
        resetErrorFields();

        // Get the values from the input text and radio fields
        var grouptitle = $(createGroupAddName).val() || "";
        var groupdescription = $(createGroupAddDescription).val() || "";
        var groupid = replaceCharacters($(createGroupAddId).val());
        var inputError = false;

        // Check if there is a group id or group title defined
        if (grouptitle === "")
        {
            setError(createGroupAddName,createGroupAddNameEmpty,true);
            inputError = true;
        } else if (grouptitle.length < 3) {
            setError(createGroupAddName,createGroupAddNameShort,true);
            inputError = true;
        }
        if (!groupid)
        {
            setError(createGroupAddId,createGroupAddIdEmpty,true);
            inputError = true;
        } else if (groupid.length < 3) {
            setError(createGroupAddId,createGroupAddIdShort,true);
            inputError = true;
        }

        if (inputError)
        {
            return;
        }
        else
        {
            // Hide the buttons and show the process status
            showProcess(true);
            groupid = 'g-' + groupid;
            doSaveGroupManagers(groupid, grouptitle, groupdescription);
        }
    };

    ////////////////////
    // Event Handlers //
    ////////////////////

    /*
     * Add jqModal functionality to the container.
     * This makes use of the jqModal (jQuery Modal) plugin that provides support
     * for lightboxes
     */
    $(createGroupContainer).jqm({
        modal: true,
        overlay: 20,
        toTop: true,
        onHide: myClose
    });

    /*
     * Add binding to the save button (create the group when you click on it)
     */
    $(createGroupAddSave).click(function(){
        saveGroup();
    });

    /*
     * When you change something in the name of the group, it first removes the bad characters
     * and then it shows the edited url in the span
     */
    $(createGroupAddName).bind("change", function(ev){
        var entered = replaceCharacters($(this).val());
        $(createGroupAddId).val(entered);
    });

    /////////////////////////////
    // Initialisation function //
    /////////////////////////////

    var doInit = function(){

        // Hide error fields at start
        $(errorFields).hide();

        // Set the text of the span containing the url of the current group
        // e.g. http://celestine.caret.local:8080/~g-
        $(createGroupAddUrl).text(sakai.api.Security.saneHTML(document.location.protocol + "//" + document.location.host + "/~g-"));
    };

    doInit();
};

sakai.api.Widgets.widgetLoader.informOnLoad("creategroup");<commit_msg>NOJIRA - Making some additions to the group creation widget<commit_after>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF 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.
 */

/*global $, Config */

var sakai = sakai || {};

/**
 * @name sakai.creategroup
 *
 * @class creategroup
 *
 * @description
 * Creategroup widget
 *
 * @version 0.0.1
 * @param {String} tuid Unique id of the widget
 * @param {Boolean} showSettings Show the settings of the widget or not
 */
sakai.creategroup = function(tuid, showSettings){

    /////////////////////////////
    // Configuration variables //
    /////////////////////////////

    // - ID
    var createGroup = "#creategroup";

    // Container
    var createGroupContainer = createGroup + "_container";

    // template containers
    var createGroupAddTemplate = "#noncourse_template_container";

    // Non course
    var createGroupAdd = createGroup + "_add";
    var createGroupAddCancel = createGroupAdd + "_cancel";
    var createGroupAddDescription = createGroupAdd + "_description";
    var createGroupAddId = createGroupAdd + "_id";
    var createGroupAddName = createGroupAdd + "_name";
    var createGroupAddProcess = createGroupAdd + "_process";
    var createGroupAddSave = createGroupAdd + "_save";
    var createGroupAddUrl = createGroupAdd + "_url";

    // Error fields
    var createGroupAddNameEmpty = createGroupAddName + "_empty";
    var createGroupAddNameShort = createGroupAddName + "_short";
    var createGroupAddIdEmpty = createGroupAddId + "_empty";
    var createGroupAddIdTaken = createGroupAddId + "_taken";
    var createGroupAddIdShort = createGroupAddId + "_short";
    var errorFields = ".creategroup_error_msg";

    // CSS Classes
    var invalidFieldClass = "invalid";

    ///////////////////////
    // Utility functions //
    ///////////////////////

    /**
     * Public function that can be called from elsewhere
     * (e.g. chat and sites widget)
     * It initializes the creategroup widget and shows the jqmodal (ligthbox)
     */
    sakai.creategroup.initialise = function(){
        $(".description_fields").show();
        $("#creategroup_add_container").show();
        $(createGroupAddTemplate).show();
        $(createGroupAddName).val("");
        $(createGroupAddDescription).val("");
        $(createGroupAddId).val("");
        $(createGroupContainer).jqmShow();
    };

    /**
     * Show or hide the process div and hide/shows the buttons
     * @param {Boolean} show
     *     true: show the process div and hide the buttons
     *     false: hide the process div and show the buttons
     */
    var showProcess = function(show){
        if(show){
            $(createGroupAddCancel).hide();
            $(createGroupAddSave).hide();
            $(createGroupAddProcess).show();
        }else{
            $(createGroupAddProcess).hide();
            $(createGroupAddCancel).show();
            $(createGroupAddSave).show();
        }
    };

    /**
     * Replace or remove malicious characters from the string
     * We use this function to modify the groupid
     * String to test against: test :?=&;\/?@+$<>#%'"''{}|\\^[]'
     * @param {Object} input The string where the characters need to be replaced
     */
    var replaceCharacters = function(input){

        input = $.trim(input); // Remove the spaces at the beginning and end of the id

        input = input.toLowerCase().replace(/ /g,"-");
        input = input.toLowerCase().replace(/'/g,"");
        input = input.toLowerCase().replace(/"/g,"");

        var regexp = new RegExp("[^a-z0-9_-]", "gi");
        input = input.replace(regexp,"_");

        return input;
    };

    ////////////////////
    // Error handling //
    ////////////////////

    var resetErrorFields = function(){
        $("input").removeClass(invalidFieldClass);
        $("textarea").removeClass(invalidFieldClass);
        $(errorFields).hide();
    };

    /**
     * Function that will visually mark a form field as an
     * invalid field.
     * @param String field
     *  JQuery selector of the input box we want to show as invalid
     * @param String errorField
     *  JQuery selector of the error message that needs to be shown.
     * @param boolean noReset
     *  Parameter that specifies whether we need to make all of the
     *  fiels valid again first
     */
    var setError = function(field,errorField, noReset){
        if (!noReset) {
            resetErrorFields();
        }
        $(field).addClass(invalidFieldClass);
        $(errorField).show();
    };

    var myClose = function(hash) {
        resetErrorFields();
        hash.o.remove();
        hash.w.hide();
    };

    ///////////////////
    // Create a group //
    ///////////////////

    /**
     * Check if the group is created correctly and exists
     * @param {String} groupid
     */
    var doCheckGroup = function(groupid){
        // Check if the group exists.
        var groupExists = false;

        $.ajax({
            url: "/~" + groupid + ".json",
            type: "GET",
            async: false,
            success: function(data, textStatus){
                groupExists = true;
            }
        });
        return groupExists;
    };

    /**
     * Create the group.
     * @param {String} groupid the id of the group that's being created
     * @param {String} grouptitle the title of the group that's being created
     * @param {String} groupdescription the description of the group that's being created
     * @param {String} groupidManagers the id of the managers group for the group that's being created
    */
    var doSaveGroup = function(groupid, grouptitle, groupdescription, groupidManagers){
    // Create a group with the managers group

        $.ajax({
            url: sakai.config.URL.GROUP_CREATE_SERVICE,
            data: {
                "_charset_":"utf-8",
                ":name": groupid,
                ":manager": groupidManagers,
                ":member": groupidManagers,
                "sakai:group-title" : grouptitle,
                "sakai:group-description" : groupdescription
            },
            type: "POST",
            success: function(data, textStatus){
                //check if the group exists
                if (doCheckGroup(groupid)) {
                    //redirect the user to the group
                    document.location = "/dev/group.html?id=" + groupid;
                }
            },
            error: function(xhr, textStatus, thrownError){
                var groupCheck = doCheckGroup(groupid);
                if (groupCheck){
                    setError(createGroupAddId,createGroupAddIdTaken,true);
                } else {
                    fluid.log("An error has occurred: " + xhr.status + " " + xhr.statusText);
                }
                showProcess(false);
            }
        });
    };

    /**
     * Create the managers group.
     * @param {String} groupid the id of the group that's being created
     * @param {String} grouptitle the title of the group that's being created
     * @param {String} groupdescription the description of the group that's being created
    */
    var doSaveGroupManagers = function(groupid, grouptitle, groupdescription){
    // Create the groups
        var groupidManagers = groupid + "-managers";
        var grouptitleManagers = grouptitle + " Managers";

        $.ajax({
            url: sakai.config.URL.GROUP_CREATE_SERVICE,
            data: {
                "_charset_":"utf-8",
                ":name": groupidManagers,
                ":manager": groupidManagers,
                ":viewer": groupidManagers,
                ":member": sakai.data.me.user.userid,
                "sakai:group-title" : grouptitleManagers,
                "sakai:group-description" : groupdescription
            },
            type: "POST",
            success: function(data, textStatus){
                // check if the managers group exists proceed
                if (doCheckGroup(groupidManagers)) {
                    // create the actual group
                    doSaveGroup(groupid, grouptitle, groupdescription, groupidManagers);
                }
            },
            error: function(xhr, textStatus, thrownError){
                var groupCheck = doCheckGroup(groupid);
                if (groupCheck){
                    setError(createGroupAddId,createGroupAddIdTaken,true);
                } else {
                    fluid.log("An error has occurred: " + xhr.status + " " + xhr.statusText);
                }
                showProcess(false);
            }
        });
    };

    var saveGroup = function(){
        resetErrorFields();

        // Get the values from the input text and radio fields
        var grouptitle = $(createGroupAddName).val() || "";
        var groupdescription = $(createGroupAddDescription).val() || "";
        var groupid = replaceCharacters($(createGroupAddId).val());
        var inputError = false;

        // Check if there is a group id or group title defined
        if (grouptitle === "")
        {
            setError(createGroupAddName,createGroupAddNameEmpty,true);
            inputError = true;
        } else if (grouptitle.length < 3) {
            setError(createGroupAddName,createGroupAddNameShort,true);
            inputError = true;
        }
        if (!groupid)
        {
            setError(createGroupAddId,createGroupAddIdEmpty,true);
            inputError = true;
        } else if (groupid.length < 3) {
            setError(createGroupAddId,createGroupAddIdShort,true);
            inputError = true;
        }

        if (inputError)
        {
            return;
        }
        else
        {
            // Hide the buttons and show the process status
            showProcess(true);
            groupid = 'g-' + groupid;
            doSaveGroupManagers(groupid, grouptitle, groupdescription);
        }
    };

    ////////////////////
    // Event Handlers //
    ////////////////////

    /*
     * Add jqModal functionality to the container.
     * This makes use of the jqModal (jQuery Modal) plugin that provides support
     * for lightboxes
     */
    $(createGroupContainer).jqm({
        modal: true,
        overlay: 20,
        toTop: true,
        onHide: myClose
    });

    /*
     * Add binding to the save button (create the group when you click on it)
     */
    $(createGroupAddSave).click(function(){
        saveGroup();
    });

    /*
     * When you change something in the name of the group, it first removes the bad characters
     * and then it shows the edited url in the span
     */
    $(createGroupAddName).bind("change", function(ev){
        var entered = replaceCharacters($(this).val());
        $(createGroupAddId).val(entered);
    });

    /////////////////////////////
    // Initialisation function //
    /////////////////////////////

    var doInit = function(){

        // Hide error fields at start
        $(errorFields).hide();

        // Set the text of the span containing the url of the current group
        // e.g. http://celestine.caret.local:8080/~g-
        $(createGroupAddUrl).text(sakai.api.Security.saneHTML(document.location.protocol + "//" + document.location.host + "/~g-"));
    };

    doInit();
};

sakai.api.Widgets.widgetLoader.informOnLoad("creategroup");<|endoftext|>