<commit_before>angular
    .module('material.components.tabs')
    .controller('MdTabsController', MdTabsController);

/**
 * @ngInject
 */
function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $mdTabInkRipple,
                           $mdUtil, $animate) {
  var ctrl     = this,
      locked   = false,
      elements = getElements(),
      queue    = [];

  ctrl.scope = $scope;
  ctrl.parent = $scope.$parent;
  ctrl.tabs = [];
  ctrl.lastSelectedIndex = null;
  ctrl.focusIndex = $scope.selectedIndex || 0;
  ctrl.offsetLeft = 0;
  ctrl.hasContent = false;
  ctrl.hasFocus = false;
  ctrl.lastClick = true;

  ctrl.redirectFocus = redirectFocus;
  ctrl.attachRipple = attachRipple;
  ctrl.shouldStretchTabs = shouldStretchTabs;
  ctrl.shouldPaginate = shouldPaginate;
  ctrl.shouldCenterTabs = shouldCenterTabs;
  ctrl.insertTab = insertTab;
  ctrl.removeTab = removeTab;
  ctrl.select = select;
  ctrl.scroll = scroll;
  ctrl.nextPage = nextPage;
  ctrl.previousPage = previousPage;
  ctrl.keydown = keydown;
  ctrl.canPageForward = canPageForward;
  ctrl.canPageBack = canPageBack;
  ctrl.refreshIndex = refreshIndex;
  ctrl.incrementSelectedIndex = incrementSelectedIndex;
  ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
  ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);

  init();

  function init () {
    $scope.$watch('selectedIndex', handleSelectedIndexChange);
    $scope.$watch('$mdTabsCtrl.focusIndex', handleFocusIndexChange);
    $scope.$watch('$mdTabsCtrl.offsetLeft', handleOffsetChange);
    $scope.$watch('$mdTabsCtrl.hasContent', handleHasContent);
    angular.element($window).on('resize', handleWindowResize);
    angular.element(elements.paging).on('DOMSubtreeModified', ctrl.updateInkBarStyles);
    $timeout(updateHeightFromContent, 0, false);
    $timeout(adjustOffset);
    $scope.$on('$destroy', cleanup);
  }

  function cleanup () {
    angular.element($window).off('resize', handleWindowResize);
    angular.element(elements.paging).off('DOMSubtreeModified', ctrl.updateInkBarStyles);
  }

  //-- Change handlers

  function handleHasContent (hasContent) {
    $element[hasContent ? 'removeClass' : 'addClass']('md-no-tab-content');
  }

  function handleOffsetChange (left) {
    var newValue = shouldCenterTabs() ? '' : '-' + left + 'px';
    angular.element(elements.paging).css('transform', 'translate3d(' + newValue + ', 0, 0)');
    $scope.$broadcast('$mdTabsPaginationChanged');
  }

  function handleFocusIndexChange (newIndex, oldIndex) {
    if (newIndex === oldIndex) return;
    if (!elements.tabs[newIndex]) return;
    adjustOffset();
    redirectFocus();
  }

  function handleSelectedIndexChange (newValue, oldValue) {
    if (newValue === oldValue) return;

    $scope.selectedIndex = getNearestSafeIndex(newValue);
    ctrl.lastSelectedIndex = oldValue;
    ctrl.updateInkBarStyles();
    updateHeightFromContent();
    $scope.$broadcast('$mdTabsChanged');
    ctrl.tabs[oldValue] && ctrl.tabs[oldValue].scope.deselect();
    ctrl.tabs[newValue] && ctrl.tabs[newValue].scope.select();
  }

  function handleResizeWhenVisible () {
    //-- if there is already a watcher waiting for resize, do nothing
    if (handleResizeWhenVisible.watcher) return;
    //-- otherwise, we will abuse the $watch function to check for visible
    handleResizeWhenVisible.watcher = $scope.$watch(function () {
      //-- since we are checking for DOM size, we use $timeout to wait for after the DOM updates
      $timeout(function () {
        //-- if the watcher has already run (ie. multiple digests in one cycle), do nothing
        if (!handleResizeWhenVisible.watcher) return;

        if ($element.prop('offsetParent')) {
          handleResizeWhenVisible.watcher();
          handleResizeWhenVisible.watcher = null;

          //-- we have to trigger our own $apply so that the DOM bindings will update
          handleWindowResize();
        }
      }, 0, false);
    });
  }

  //-- Event handlers / actions

  function keydown (event) {
    switch (event.keyCode) {
      case $mdConstant.KEY_CODE.LEFT_ARROW:
        event.preventDefault();
        incrementSelectedIndex(-1, true);
        break;
      case $mdConstant.KEY_CODE.RIGHT_ARROW:
        event.preventDefault();
        incrementSelectedIndex(1, true);
        break;
      case $mdConstant.KEY_CODE.SPACE:
      case $mdConstant.KEY_CODE.ENTER:
        event.preventDefault();
        if (!locked) $scope.selectedIndex = ctrl.focusIndex;
        break;
    }
    ctrl.lastClick = false;
  }

  function select (index) {
    if (!locked) ctrl.focusIndex = $scope.selectedIndex = index;
    ctrl.lastClick = true;
    ctrl.tabs[index].element.triggerHandler('click');
  }

  function scroll (event) {
    if (!shouldPaginate()) return;
    event.preventDefault();
    ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
  }

  function nextPage () {
    var viewportWidth = elements.canvas.clientWidth,
        totalWidth = viewportWidth + ctrl.offsetLeft,
        i, tab;
    for (i = 0; i < elements.tabs.length; i++) {
      tab = elements.tabs[i];
      if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
    }
    ctrl.offsetLeft = fixOffset(tab.offsetLeft);
  }

  function previousPage () {
    var i, tab;
    for (i = 0; i < elements.tabs.length; i++) {
      tab = elements.tabs[i];
      if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
    }
    ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
  }

  function handleWindowResize () {
    $scope.$apply(function () {
      ctrl.lastSelectedIndex = $scope.selectedIndex;
      ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
      $timeout(ctrl.updateInkBarStyles, 0, false);
    });
  }

  function removeTab (tabData) {
    var selectedIndex = $scope.selectedIndex,
        tab = ctrl.tabs.splice(tabData.getIndex(), 1)[0];
    refreshIndex();
    //-- when removing a tab, if the selected index did not change, we have to manually trigger the
    //   tab select/deselect events
    if ($scope.selectedIndex === selectedIndex) {
      tab.scope.deselect();
      ctrl.tabs[$scope.selectedIndex] && ctrl.tabs[$scope.selectedIndex].scope.select();
    }
    $timeout(function () {
      ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
    });
  }

  function insertTab (tabData, index) {
    var proto = {
          getIndex: function () { return ctrl.tabs.indexOf(tab); },
          isActive: function () { return this.getIndex() === $scope.selectedIndex; },
          isLeft:   function () { return this.getIndex() < $scope.selectedIndex; },
          isRight:  function () { return this.getIndex() > $scope.selectedIndex; },
          hasFocus: function () { return !ctrl.lastClick && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; },
          id:       $mdUtil.nextUid()
        },
        tab = angular.extend(proto, tabData);
    if (angular.isDefined(index)) {
      ctrl.tabs.splice(index, 0, tab);
    } else {
      ctrl.tabs.push(tab);
    }
    processQueue();
    updateHasContent();
    return tab;
  }

  //-- Getter methods

  function getElements () {
    var elements      = {};

    //-- gather tab bar elements
    elements.wrapper  = $element[0].getElementsByTagName('md-tabs-wrapper')[0];
    elements.canvas   = elements.wrapper.getElementsByTagName('md-tabs-canvas')[0];
    elements.paging   = elements.canvas.getElementsByTagName('md-pagination-wrapper')[0];
    elements.tabs     = elements.paging.getElementsByTagName('md-tab-item');
    elements.dummies  = elements.canvas.getElementsByTagName('md-dummy-tab');
    elements.inkBar   = elements.paging.getElementsByTagName('md-ink-bar')[0];

    //-- gather tab content elements
    elements.contentsWrapper = $element[0].getElementsByTagName('md-tabs-content-wrapper')[0];
    elements.contents = elements.contentsWrapper.getElementsByTagName('md-tab-content');

    return elements;
  }

  function canPageBack () {
    return ctrl.offsetLeft > 0;
  }

  function canPageForward () {
    var lastTab = elements.tabs[elements.tabs.length - 1];
    return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft;
  }

  function shouldStretchTabs () {
    switch ($scope.stretchTabs) {
      case 'always': return true;
      case 'never':  return false;
      default:       return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches;
    }
  }

  function shouldCenterTabs () {
    return $scope.centerTabs && !shouldPaginate();
  }

  function shouldPaginate () {
    if ($scope.noPagination) return false;
    var canvasWidth = $element.prop('clientWidth');
    angular.forEach(elements.tabs, function (tab) { canvasWidth -= tab.offsetWidth; });
    return canvasWidth < 0;
  }

  function getNearestSafeIndex(newIndex) {
    var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
        i, tab;
    for (i = 0; i <= maxOffset; i++) {
      tab = ctrl.tabs[newIndex + i];
      if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
      tab = ctrl.tabs[newIndex - i];
      if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
    }
    return newIndex;
  }

  //-- Utility methods

  function updateTabOrder () {
    var selectedItem = ctrl.tabs[$scope.selectedIndex],
        focusItem = ctrl.tabs[ctrl.focusIndex];
    ctrl.tabs = ctrl.tabs.sort(function (a, b) {
      return a.index - b.index;
    });
    $scope.selectedIndex = ctrl.tabs.indexOf(selectedItem);
    ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
  }

  function incrementSelectedIndex (inc, focus) {
    var newIndex,
        index = focus ? ctrl.focusIndex : $scope.selectedIndex;
    for (newIndex = index + inc;
         ctrl.tabs[newIndex] && ctrl.tabs[newIndex].scope.disabled;
         newIndex += inc) {}
    if (ctrl.tabs[newIndex]) {
      if (focus) ctrl.focusIndex = newIndex;
      else $scope.selectedIndex = newIndex;
    }
  }

  function redirectFocus () {
    elements.dummies[ctrl.focusIndex].focus();
  }

  function adjustOffset () {
    if (shouldCenterTabs()) return;
    var tab = elements.tabs[ctrl.focusIndex],
        left = tab.offsetLeft,
        right = tab.offsetWidth + left;
    ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth));
    ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
  }

  function processQueue () {
    queue.forEach(function (func) { $timeout(func); });
    queue = [];
  }

  function updateHasContent () {
    var hasContent = false;
    angular.forEach(ctrl.tabs, function (tab) {
      if (tab.template) hasContent = true;
    });
    ctrl.hasContent = hasContent;
  }

  function refreshIndex () {
    $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex);
    ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
  }

  function updateHeightFromContent () {
    if (!$scope.dynamicHeight) return $element.css('height', '');
    if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
    var tabContent    = elements.contents[$scope.selectedIndex],
        contentHeight = tabContent ? tabContent.offsetHeight : 0,
        tabsHeight    = elements.wrapper.offsetHeight,
        newHeight     = contentHeight + tabsHeight,
        currentHeight = $element.prop('clientHeight');
    if (currentHeight === newHeight) return;
    locked = true;
    $animate
        .animate(
          $element,
          { height: currentHeight + 'px' },
          { height: newHeight + 'px'}
        )
        .then(function () {
          $element.css('height', '');
          locked = false;
        });
  }

  function updateInkBarStyles () {
    if (!elements.tabs[$scope.selectedIndex]) return;
    if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
    //-- if the element is not visible, we will not be able to calculate sizes until it is
    //-- we should treat that as a resize event rather than just updating the ink bar
    if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
    var index = $scope.selectedIndex,
        totalWidth = elements.paging.offsetWidth,
        tab = elements.tabs[index],
        left = tab.offsetLeft,
        right = totalWidth - left - tab.offsetWidth;
    updateInkBarClassName();
    angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
  }

  function updateInkBarClassName () {
    var newIndex = $scope.selectedIndex,
        oldIndex = ctrl.lastSelectedIndex,
        ink = angular.element(elements.inkBar);
    ink.removeClass('md-left md-right');
    if (!angular.isNumber(oldIndex)) return;
    if (newIndex < oldIndex) {
      ink.addClass('md-left');
    } else if (newIndex > oldIndex) {
      ink.addClass('md-right');
    }
  }

  function fixOffset (value) {
    if (!elements.tabs.length || !shouldPaginate()) return 0;
    var lastTab = elements.tabs[elements.tabs.length - 1],
        totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
    value = Math.max(0, value);
    value = Math.min(totalWidth - elements.canvas.clientWidth, value);
    return value;
  }

  function attachRipple (scope, element) {
    var options = { colorElement: angular.element(elements.inkBar) };
    $mdTabInkRipple.attach(scope, element, options);
  }
}
<commit_msg>fix(tabs): prevents select/deselect events from firing on destroy<commit_after>angular
    .module('material.components.tabs')
    .controller('MdTabsController', MdTabsController);

/**
 * @ngInject
 */
function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $mdTabInkRipple,
                           $mdUtil, $animate) {
  var ctrl      = this,
      locked    = false,
      elements  = getElements(),
      queue     = [],
      destroyed = false;

  ctrl.scope = $scope;
  ctrl.parent = $scope.$parent;
  ctrl.tabs = [];
  ctrl.lastSelectedIndex = null;
  ctrl.focusIndex = $scope.selectedIndex || 0;
  ctrl.offsetLeft = 0;
  ctrl.hasContent = false;
  ctrl.hasFocus = false;
  ctrl.lastClick = true;

  ctrl.redirectFocus = redirectFocus;
  ctrl.attachRipple = attachRipple;
  ctrl.shouldStretchTabs = shouldStretchTabs;
  ctrl.shouldPaginate = shouldPaginate;
  ctrl.shouldCenterTabs = shouldCenterTabs;
  ctrl.insertTab = insertTab;
  ctrl.removeTab = removeTab;
  ctrl.select = select;
  ctrl.scroll = scroll;
  ctrl.nextPage = nextPage;
  ctrl.previousPage = previousPage;
  ctrl.keydown = keydown;
  ctrl.canPageForward = canPageForward;
  ctrl.canPageBack = canPageBack;
  ctrl.refreshIndex = refreshIndex;
  ctrl.incrementSelectedIndex = incrementSelectedIndex;
  ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
  ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);

  init();

  function init () {
    $scope.$watch('selectedIndex', handleSelectedIndexChange);
    $scope.$watch('$mdTabsCtrl.focusIndex', handleFocusIndexChange);
    $scope.$watch('$mdTabsCtrl.offsetLeft', handleOffsetChange);
    $scope.$watch('$mdTabsCtrl.hasContent', handleHasContent);
    angular.element($window).on('resize', handleWindowResize);
    angular.element(elements.paging).on('DOMSubtreeModified', ctrl.updateInkBarStyles);
    $timeout(updateHeightFromContent, 0, false);
    $timeout(adjustOffset);
    $scope.$on('$destroy', cleanup);
  }

  function cleanup () {
    destroyed = true;
    angular.element($window).off('resize', handleWindowResize);
    angular.element(elements.paging).off('DOMSubtreeModified', ctrl.updateInkBarStyles);
  }

  //-- Change handlers

  function handleHasContent (hasContent) {
    $element[hasContent ? 'removeClass' : 'addClass']('md-no-tab-content');
  }

  function handleOffsetChange (left) {
    var newValue = shouldCenterTabs() ? '' : '-' + left + 'px';
    angular.element(elements.paging).css('transform', 'translate3d(' + newValue + ', 0, 0)');
    $scope.$broadcast('$mdTabsPaginationChanged');
  }

  function handleFocusIndexChange (newIndex, oldIndex) {
    if (newIndex === oldIndex) return;
    if (!elements.tabs[newIndex]) return;
    adjustOffset();
    redirectFocus();
  }

  function handleSelectedIndexChange (newValue, oldValue) {
    if (newValue === oldValue) return;

    $scope.selectedIndex = getNearestSafeIndex(newValue);
    ctrl.lastSelectedIndex = oldValue;
    ctrl.updateInkBarStyles();
    updateHeightFromContent();
    $scope.$broadcast('$mdTabsChanged');
    ctrl.tabs[oldValue] && ctrl.tabs[oldValue].scope.deselect();
    ctrl.tabs[newValue] && ctrl.tabs[newValue].scope.select();
  }

  function handleResizeWhenVisible () {
    //-- if there is already a watcher waiting for resize, do nothing
    if (handleResizeWhenVisible.watcher) return;
    //-- otherwise, we will abuse the $watch function to check for visible
    handleResizeWhenVisible.watcher = $scope.$watch(function () {
      //-- since we are checking for DOM size, we use $timeout to wait for after the DOM updates
      $timeout(function () {
        //-- if the watcher has already run (ie. multiple digests in one cycle), do nothing
        if (!handleResizeWhenVisible.watcher) return;

        if ($element.prop('offsetParent')) {
          handleResizeWhenVisible.watcher();
          handleResizeWhenVisible.watcher = null;

          //-- we have to trigger our own $apply so that the DOM bindings will update
          handleWindowResize();
        }
      }, 0, false);
    });
  }

  //-- Event handlers / actions

  function keydown (event) {
    switch (event.keyCode) {
      case $mdConstant.KEY_CODE.LEFT_ARROW:
        event.preventDefault();
        incrementSelectedIndex(-1, true);
        break;
      case $mdConstant.KEY_CODE.RIGHT_ARROW:
        event.preventDefault();
        incrementSelectedIndex(1, true);
        break;
      case $mdConstant.KEY_CODE.SPACE:
      case $mdConstant.KEY_CODE.ENTER:
        event.preventDefault();
        if (!locked) $scope.selectedIndex = ctrl.focusIndex;
        break;
    }
    ctrl.lastClick = false;
  }

  function select (index) {
    if (!locked) ctrl.focusIndex = $scope.selectedIndex = index;
    ctrl.lastClick = true;
    ctrl.tabs[index].element.triggerHandler('click');
  }

  function scroll (event) {
    if (!shouldPaginate()) return;
    event.preventDefault();
    ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
  }

  function nextPage () {
    var viewportWidth = elements.canvas.clientWidth,
        totalWidth = viewportWidth + ctrl.offsetLeft,
        i, tab;
    for (i = 0; i < elements.tabs.length; i++) {
      tab = elements.tabs[i];
      if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
    }
    ctrl.offsetLeft = fixOffset(tab.offsetLeft);
  }

  function previousPage () {
    var i, tab;
    for (i = 0; i < elements.tabs.length; i++) {
      tab = elements.tabs[i];
      if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
    }
    ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
  }

  function handleWindowResize () {
    $scope.$apply(function () {
      ctrl.lastSelectedIndex = $scope.selectedIndex;
      ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
      $timeout(ctrl.updateInkBarStyles, 0, false);
    });
  }

  function removeTab (tabData) {
    var selectedIndex = $scope.selectedIndex,
        tab = ctrl.tabs.splice(tabData.getIndex(), 1)[0];
    refreshIndex();
    //-- when removing a tab, if the selected index did not change, we have to manually trigger the
    //   tab select/deselect events
    if ($scope.selectedIndex === selectedIndex && !destroyed) {
      tab.scope.deselect();
      ctrl.tabs[$scope.selectedIndex] && ctrl.tabs[$scope.selectedIndex].scope.select();
    }
    $timeout(function () {
      ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
    });
  }

  function insertTab (tabData, index) {
    var proto = {
          getIndex: function () { return ctrl.tabs.indexOf(tab); },
          isActive: function () { return this.getIndex() === $scope.selectedIndex; },
          isLeft:   function () { return this.getIndex() < $scope.selectedIndex; },
          isRight:  function () { return this.getIndex() > $scope.selectedIndex; },
          hasFocus: function () { return !ctrl.lastClick && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; },
          id:       $mdUtil.nextUid()
        },
        tab = angular.extend(proto, tabData);
    if (angular.isDefined(index)) {
      ctrl.tabs.splice(index, 0, tab);
    } else {
      ctrl.tabs.push(tab);
    }
    processQueue();
    updateHasContent();
    return tab;
  }

  //-- Getter methods

  function getElements () {
    var elements      = {};

    //-- gather tab bar elements
    elements.wrapper  = $element[0].getElementsByTagName('md-tabs-wrapper')[0];
    elements.canvas   = elements.wrapper.getElementsByTagName('md-tabs-canvas')[0];
    elements.paging   = elements.canvas.getElementsByTagName('md-pagination-wrapper')[0];
    elements.tabs     = elements.paging.getElementsByTagName('md-tab-item');
    elements.dummies  = elements.canvas.getElementsByTagName('md-dummy-tab');
    elements.inkBar   = elements.paging.getElementsByTagName('md-ink-bar')[0];

    //-- gather tab content elements
    elements.contentsWrapper = $element[0].getElementsByTagName('md-tabs-content-wrapper')[0];
    elements.contents = elements.contentsWrapper.getElementsByTagName('md-tab-content');

    return elements;
  }

  function canPageBack () {
    return ctrl.offsetLeft > 0;
  }

  function canPageForward () {
    var lastTab = elements.tabs[elements.tabs.length - 1];
    return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft;
  }

  function shouldStretchTabs () {
    switch ($scope.stretchTabs) {
      case 'always': return true;
      case 'never':  return false;
      default:       return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches;
    }
  }

  function shouldCenterTabs () {
    return $scope.centerTabs && !shouldPaginate();
  }

  function shouldPaginate () {
    if ($scope.noPagination) return false;
    var canvasWidth = $element.prop('clientWidth');
    angular.forEach(elements.tabs, function (tab) { canvasWidth -= tab.offsetWidth; });
    return canvasWidth < 0;
  }

  function getNearestSafeIndex(newIndex) {
    var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
        i, tab;
    for (i = 0; i <= maxOffset; i++) {
      tab = ctrl.tabs[newIndex + i];
      if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
      tab = ctrl.tabs[newIndex - i];
      if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
    }
    return newIndex;
  }

  //-- Utility methods

  function updateTabOrder () {
    var selectedItem = ctrl.tabs[$scope.selectedIndex],
        focusItem = ctrl.tabs[ctrl.focusIndex];
    ctrl.tabs = ctrl.tabs.sort(function (a, b) {
      return a.index - b.index;
    });
    $scope.selectedIndex = ctrl.tabs.indexOf(selectedItem);
    ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
  }

  function incrementSelectedIndex (inc, focus) {
    var newIndex,
        index = focus ? ctrl.focusIndex : $scope.selectedIndex;
    for (newIndex = index + inc;
         ctrl.tabs[newIndex] && ctrl.tabs[newIndex].scope.disabled;
         newIndex += inc) {}
    if (ctrl.tabs[newIndex]) {
      if (focus) ctrl.focusIndex = newIndex;
      else $scope.selectedIndex = newIndex;
    }
  }

  function redirectFocus () {
    elements.dummies[ctrl.focusIndex].focus();
  }

  function adjustOffset () {
    if (shouldCenterTabs()) return;
    var tab = elements.tabs[ctrl.focusIndex],
        left = tab.offsetLeft,
        right = tab.offsetWidth + left;
    ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth));
    ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
  }

  function processQueue () {
    queue.forEach(function (func) { $timeout(func); });
    queue = [];
  }

  function updateHasContent () {
    var hasContent = false;
    angular.forEach(ctrl.tabs, function (tab) {
      if (tab.template) hasContent = true;
    });
    ctrl.hasContent = hasContent;
  }

  function refreshIndex () {
    $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex);
    ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
  }

  function updateHeightFromContent () {
    if (!$scope.dynamicHeight) return $element.css('height', '');
    if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
    var tabContent    = elements.contents[$scope.selectedIndex],
        contentHeight = tabContent ? tabContent.offsetHeight : 0,
        tabsHeight    = elements.wrapper.offsetHeight,
        newHeight     = contentHeight + tabsHeight,
        currentHeight = $element.prop('clientHeight');
    if (currentHeight === newHeight) return;
    locked = true;
    $animate
        .animate(
          $element,
          { height: currentHeight + 'px' },
          { height: newHeight + 'px'}
        )
        .then(function () {
          $element.css('height', '');
          locked = false;
        });
  }

  function updateInkBarStyles () {
    if (!elements.tabs[$scope.selectedIndex]) return;
    if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
    //-- if the element is not visible, we will not be able to calculate sizes until it is
    //-- we should treat that as a resize event rather than just updating the ink bar
    if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
    var index = $scope.selectedIndex,
        totalWidth = elements.paging.offsetWidth,
        tab = elements.tabs[index],
        left = tab.offsetLeft,
        right = totalWidth - left - tab.offsetWidth;
    updateInkBarClassName();
    angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
  }

  function updateInkBarClassName () {
    var newIndex = $scope.selectedIndex,
        oldIndex = ctrl.lastSelectedIndex,
        ink = angular.element(elements.inkBar);
    ink.removeClass('md-left md-right');
    if (!angular.isNumber(oldIndex)) return;
    if (newIndex < oldIndex) {
      ink.addClass('md-left');
    } else if (newIndex > oldIndex) {
      ink.addClass('md-right');
    }
  }

  function fixOffset (value) {
    if (!elements.tabs.length || !shouldPaginate()) return 0;
    var lastTab = elements.tabs[elements.tabs.length - 1],
        totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
    value = Math.max(0, value);
    value = Math.min(totalWidth - elements.canvas.clientWidth, value);
    return value;
  }

  function attachRipple (scope, element) {
    var options = { colorElement: angular.element(elements.inkBar) };
    $mdTabInkRipple.attach(scope, element, options);
  }
}
<|endoftext|>


<commit_before>/*
---

name: View

description: Provides an child that handles an area in which a user can
             interract.

license: MIT-style license.

authors:
	- Jean-Philippe Dery (jeanphilippe.dery@gmail.com)

requires:
	- Component

provides:
	- View

...
*/

/**
 * @see    http://moobilejs.com/doc/latest/View/View
 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
 * @edited 0.2.0
 * @since  0.1.0
 */
Moobile.View = new Class({

	Extends: Moobile.Component,

	/**
	 * @hidden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	_layout: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#contentElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	contentElement: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#contentWrapperElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	contentWrapperElement: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#options
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	options: {
		layout: 'vertical'
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @edited 0.2.0
	 * @since  0.1.0
	 */
	willBuild: function() {

		this.parent();

		this.element.addClass('view');

		var content = this.getRoleElement('content') /* <0.1-compat> */ || this.getRoleElement('view-content') /* </0.1-compat> */;
		if (content === null) {
			content = document.createElement('div');
			content.ingest(this.element);
			content.inject(this.element);
			content.setRole('content');
		}

		var wrapper = this.getRoleElement('content-wrapper');
		if (wrapper === null) {
			wrapper = document.createElement('div');
			wrapper.wraps(content);
			wrapper.setRole('content-wrapper');
		}
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	didBuild: function() {

		this.parent();

		var classes = this.element.get('class');
		if (classes) {
			classes.split(' ').each(function(klass) {
				klass = klass.trim();
				if (klass) this.contentElement.addClass(klass + '-content');
			}, this);
		}

		this.setLayout(this.options.layout);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	destroy: function() {
		this.contentElement = null;
		this.parent();
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#enableTouch
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	enableTouch: function() {
		this.element.removeClass('disable').addClass('enable');
		return this;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#disableTouch
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	disableTouch: function() {
		this.element.removeClass('enable').addClass('disable');
		return this;
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	addChildComponent: function(component, where) {
		if (where === 'header') return this.parent(component, 'top');
		if (where === 'footer') return this.parent(component, 'bottom');
		return this.addChildComponentInside(component, this.contentElement, where);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willAddChildComponent: function(component) {
		this.parent(component);
		component.setParentView(this);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willRemoveChildComponent: function(component) {
		this.parent(component);
		component.setParentView(null);
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#getContentElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	getContentElement: function() {
		return this.contentElement;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#getContentWrapperElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	getContentWrapperElement: function() {
		return this.contentWrapperElement;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#setLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	setLayout: function(layout) {

		if (this._layout === layout)
			return this;

		if (layout) {
			this.willChangeLayout(layout);
			this.element.removeClass('view-layout-' + this._layout).addClass('view-layout-' + layout);
			this._layout = layout;
			this.didChangeLayout(layout);
		}

		return this;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#setLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	getLayout: function() {
		return this._layout;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#willChangeLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	willChangeLayout: function(layout) {

	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#didChangeLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	didChangeLayout: function(layout) {

	}

});

Class.refactor(Moobile.Component, {

	/**
	 * @hidden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	_parentView: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#setParentView
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	setParentView: function(parentView) {

		if (this._parentView === parentView)
			return this;

		this.parentViewWillChange(parentView);
		this._parentView = parentView;
		this.parentViewDidChange(parentView);

		if (this instanceof Moobile.View)
			return this;

		var by = function(component) {
			return !(component instanceof Moobile.View);
		};

		this.getChildComponents().filter(by).invoke('setParentView', parentView);

		return this;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#getParentView
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	getParentView: function() {
		return this._parentView;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#parentViewDidChange
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	parentViewWillChange: function(parentView) {

	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#parentViewDidChange
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	parentViewDidChange: function(parentView) {

	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willAddChildComponent: function(component) {
		this.previous(component);
		component.setParentView(this._parentView);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willRemoveChildComponent: function(component) {
		this.previous(component);
		component.setParentView(null);
	}

});

/**
 * @see    http://moobilejs.com/doc/latest/View/View#MoobileViewAt
 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
 * @since  0.1.0
 */
Moobile.View.at = function(path) {

	var element = Element.at(path);
	if (element) {
		return Moobile.Component.create(Moobile.View, element, 'data-view');
	}

	return null;
};

//------------------------------------------------------------------------------
// Roles
//------------------------------------------------------------------------------

Moobile.Component.defineRole('view', null, null, function(element) {
	this.addChildComponent(Moobile.Component.create(Moobile.View, element, 'data-view'));
});

Moobile.Component.defineRole('content', Moobile.View, {traversable: true}, function(element) {
	this.contentElement = element;
	this.contentElement.addClass('view-content');
});

Moobile.Component.defineRole('content-wrapper', Moobile.View, {traversable: true}, function(element) {
	this.contentWrapperElement = element
	this.contentWrapperElement.addClass('view-content-wrapper');
});

// <0.1-compat>
Moobile.Component.defineRole('view-content', Moobile.View, {traversable: true}, function(element) {
	console.log('[DEPRECATION NOTICE] The role "view-content" will be removed in 0.4, use the role "content" instead');
	this.contentElement = element;
	this.contentElement.addClass('view-content');
});
// </0.1-compat>

//------------------------------------------------------------------------------
// Styles
//------------------------------------------------------------------------------

Moobile.Component.defineStyle('dark', Moobile.View, {
	attach: function(element) { element.addClass('style-dark'); },
	detach: function(element) { element.removeClass('style-dark'); }
});
<commit_msg>Updated the layout class setting process<commit_after>/*
---

name: View

description: Provides an child that handles an area in which a user can
             interract.

license: MIT-style license.

authors:
	- Jean-Philippe Dery (jeanphilippe.dery@gmail.com)

requires:
	- Component

provides:
	- View

...
*/

/**
 * @see    http://moobilejs.com/doc/latest/View/View
 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
 * @edited 0.2.0
 * @since  0.1.0
 */
Moobile.View = new Class({

	Extends: Moobile.Component,

	/**
	 * @hidden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	_layout: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#contentElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	contentElement: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#contentWrapperElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	contentWrapperElement: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#options
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	options: {
		layout: 'vertical'
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @edited 0.2.0
	 * @since  0.1.0
	 */
	willBuild: function() {

		this.parent();

		this.element.addClass('view');

		var content = this.getRoleElement('content') /* <0.1-compat> */ || this.getRoleElement('view-content') /* </0.1-compat> */;
		if (content === null) {
			content = document.createElement('div');
			content.ingest(this.element);
			content.inject(this.element);
			content.setRole('content');
		}

		var wrapper = this.getRoleElement('content-wrapper');
		if (wrapper === null) {
			wrapper = document.createElement('div');
			wrapper.wraps(content);
			wrapper.setRole('content-wrapper');
		}
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	didBuild: function() {

		this.parent();

		var classes = this.element.get('class');
		if (classes) {
			classes.split(' ').each(function(klass) {
				klass = klass.trim();
				if (klass) this.contentElement.addClass(klass + '-content');
			}, this);
		}

		this.setLayout(this.options.layout);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	destroy: function() {
		this.contentElement = null;
		this.parent();
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#enableTouch
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	enableTouch: function() {
		this.element.removeClass('disable').addClass('enable');
		return this;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#disableTouch
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	disableTouch: function() {
		this.element.removeClass('enable').addClass('disable');
		return this;
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	addChildComponent: function(component, where) {
		if (where === 'header') return this.parent(component, 'top');
		if (where === 'footer') return this.parent(component, 'bottom');
		return this.addChildComponentInside(component, this.contentElement, where);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willAddChildComponent: function(component) {
		this.parent(component);
		component.setParentView(this);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willRemoveChildComponent: function(component) {
		this.parent(component);
		component.setParentView(null);
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#getContentElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	getContentElement: function() {
		return this.contentElement;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#getContentWrapperElement
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	getContentWrapperElement: function() {
		return this.contentWrapperElement;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#setLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	setLayout: function(layout) {

		if (this._layout === layout)
			return this;

		this.willChangeLayout(layout);
		if (this._layout) this.element.removeClass('view-layout-' + this._layout);
		this._layout = layout;
		if (this._layout) this.element.addClass('view-layout-' + this._layout);
		this.didChangeLayout(layout);

		return this;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#setLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	getLayout: function() {
		return this._layout;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#willChangeLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	willChangeLayout: function(layout) {

	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#didChangeLayout
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.2.0
	 */
	didChangeLayout: function(layout) {

	}

});

Class.refactor(Moobile.Component, {

	/**
	 * @hidden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	_parentView: null,

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#setParentView
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	setParentView: function(parentView) {

		if (this._parentView === parentView)
			return this;

		this.parentViewWillChange(parentView);
		this._parentView = parentView;
		this.parentViewDidChange(parentView);

		if (this instanceof Moobile.View)
			return this;

		var by = function(component) {
			return !(component instanceof Moobile.View);
		};

		this.getChildComponents().filter(by).invoke('setParentView', parentView);

		return this;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#getParentView
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	getParentView: function() {
		return this._parentView;
	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#parentViewDidChange
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	parentViewWillChange: function(parentView) {

	},

	/**
	 * @see    http://moobilejs.com/doc/latest/View/View#parentViewDidChange
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	parentViewDidChange: function(parentView) {

	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willAddChildComponent: function(component) {
		this.previous(component);
		component.setParentView(this._parentView);
	},

	/**
	 * @overridden
	 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
	 * @since  0.1.0
	 */
	willRemoveChildComponent: function(component) {
		this.previous(component);
		component.setParentView(null);
	}

});

/**
 * @see    http://moobilejs.com/doc/latest/View/View#MoobileViewAt
 * @author Jean-Philippe Dery (jeanphilippe.dery@gmail.com)
 * @since  0.1.0
 */
Moobile.View.at = function(path) {

	var element = Element.at(path);
	if (element) {
		return Moobile.Component.create(Moobile.View, element, 'data-view');
	}

	return null;
};

//------------------------------------------------------------------------------
// Roles
//------------------------------------------------------------------------------

Moobile.Component.defineRole('view', null, null, function(element) {
	this.addChildComponent(Moobile.Component.create(Moobile.View, element, 'data-view'));
});

Moobile.Component.defineRole('content', Moobile.View, {traversable: true}, function(element) {
	this.contentElement = element;
	this.contentElement.addClass('view-content');
});

Moobile.Component.defineRole('content-wrapper', Moobile.View, {traversable: true}, function(element) {
	this.contentWrapperElement = element
	this.contentWrapperElement.addClass('view-content-wrapper');
});

// <0.1-compat>
Moobile.Component.defineRole('view-content', Moobile.View, {traversable: true}, function(element) {
	console.log('[DEPRECATION NOTICE] The role "view-content" will be removed in 0.4, use the role "content" instead');
	this.contentElement = element;
	this.contentElement.addClass('view-content');
});
// </0.1-compat>

//------------------------------------------------------------------------------
// Styles
//------------------------------------------------------------------------------

Moobile.Component.defineStyle('dark', Moobile.View, {
	attach: function(element) { element.addClass('style-dark'); },
	detach: function(element) { element.removeClass('style-dark'); }
});
<|endoftext|>


<commit_before>exports = {
    /**
     * Tests a non global value to see if its defined
     *
     * @param {*} value - the value to test if defined
     *
     * @returns {Boolean} - true|false if the value is set
     */
    isset: function(value){
        return typeof value != "undefined";
    },

    /**
     * Parses the query param string on the window url
     *
     * @returns {object} - a map of any params found
     */
    parseQueryParams: function(){
        var cParams = {};

        var sQueryString = window.location.search;

        if (sQueryString)
        {
            var aQueryParams = sQueryString.substring(1).split("&");

            aQueryParams.forEach(function(sQueryParam){
                var aParts = sQueryParam.split("=");
                cParams[aParts[0]] = aParts[1];
            });
        }

        return cParams;
    },

    /**
     * Extend an object with other objects
     * Thanks to
     *      stackoverflow http://stackoverflow.com/questions/9399365/deep-extend-like-jquerys-for-nodejs
     *      jQuery https://github.com/jquery/jquery/blob/master/src/core.js
     *
     * @returns {object} - the extended target
     */
    extend: function(){
        var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
                i = 1,
                length = arguments.length,
                deep = false,
                toString = Object.prototype.toString,
                hasOwn = Object.prototype.hasOwnProperty,
                push = Array.prototype.push,
                slice = Array.prototype.slice,
                trim = String.prototype.trim,
                indexOf = Array.prototype.indexOf,
                class2type = {
            "[object Boolean]": "boolean",
            "[object Number]": "number",
            "[object String]": "string",
            "[object Function]": "function",
            "[object Array]": "array",
            "[object Date]": "date",
            "[object RegExp]": "regexp",
            "[object Object]": "object"
        },
        jQuery = {
            isFunction: function(obj) {
                return jQuery.type(obj) === "function";
            },
            isArray: Array.isArray ||
                    function(obj) {
                        return jQuery.type(obj) === "array";
                    },
            isWindow: function(obj) {
                return obj != null && obj == obj.window;
            },
            isNumeric: function(obj) {
                return !isNaN(parseFloat(obj)) && isFinite(obj);
            },
            type: function(obj) {
                return obj == null ? String(obj) : class2type[toString.call(obj)] || "object";
            },
            isPlainObject: function(obj) {
                if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
                    return false;
                }
                try {
                    if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
                        return false;
                    };
                } catch (e) {
                    return false;
                }
                var key;
                for (key in obj) {
                }
                return key === undefined || hasOwn.call(obj, key)
            }
        };
        if (typeof target === "boolean") {
            deep = target;
            target = arguments[1] || {};
            i = 2;
        }
        if (typeof target !== "object" && !jQuery.isFunction(target)) {
            target = {}
        }
        if (length === i) {
            target = this;
            --i;
        }
        for (i; i < length; i++) {
            if ((options = arguments[i]) != null) {
                for (name in options) {
                    src = target[name];
                    copy = options[name];
                    if (target === copy) {
                        continue
                    }
                    if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
                        if (copyIsArray) {
                            copyIsArray = false;
                            clone = src && jQuery.isArray(src) ? src : []
                        } else {
                            clone = src && jQuery.isPlainObject(src) ? src : {};
                        }
                        // WARNING: RECURSION
                        target[name] = extend(deep, clone, copy);
                    } else if (copy !== undefined) {
                        target[name] = copy;
                    }
                }
            }
        }
        return target;
    },

    /**
     *
     * @callback AjaxCompleteCallback
     *
     * @param {Error} cErr
     * @param {*} response
     */

    /**
     * Run an ajax request for a resource
     *
     * @param {object} cOptions
     * @param {string} cOptions.src - the url to load
     * @param {AjaxCompleteCallback} cOptions.onComplete - callback on success or failure
     * @param {string} cOptions.type - GET | POST for request type
     *
     */
    ajaxLoad: function(cOptions){
        var cDefaults = {
            src: null,
            onComplete: function(){},
            type: "GET",
            dataType: "json"
        };

        cOptions = extend({}, cDefaults, cOptions);

        if (cOptions.src)
        {
            var cRequest = new XMLHttpRequest();

            cRequest.onreadystatechange = function(){
                if (cRequest.readyState == 4)
                {
                    if (cRequest.status == 200)
                    {
                        var value = cRequest.response;

                        switch (cOptions.dataType)
                        {
                            case "json":
                                try
                                {
                                    cOptions.onComplete(null, JSON.parse(value));
                                }
                                catch (e)
                                {
                                    cOptions.onComplete(e);
                                }

                                break
                            default:
                                cOptions.onComplete(null, value);
                                break;
                        }
                    }
                    else
                    {
                        var cError = new Error("Failed GET request with status " + cRequest.status + " at url: " + cOptions.src);
                        cOptions.onComplete(cError);
                    }
                }
            };

            cRequest.open("GET", cOptions.src, true);
            cRequest.send();
        }
        else
        {
            cOptions.onComplete(new Error("Missing source parameter"));
        }
    },

    inherits: function(child, parent){
        function Inherited(){};
        Inherited.prototype = parent.prototype;

        child.prototype = new Inherited();
        child.prototype.constructor = child;

        for (var sKey in parent.prototype)
        {
            if (sKey.indexOf("_super") == -1)
            {
                Inherited.prototype["_super" + sKey] = (function(sKey){
                    return function(){
                        return parent.prototype[sKey].apply(this, arguments);
                    };
                })(sKey);
            }
        }
    },

    mixin: function(cObj, cMixin){
        for (var sKey in cMixin)
        {
            cObj.prototype[sKey] = cMixin[sKey];
        }
    }
};

//# sourceURL=engine/base/base.js<commit_msg>fix for super functions scope<commit_after>exports = {
    /**
     * Tests a non global value to see if its defined
     *
     * @param {*} value - the value to test if defined
     *
     * @returns {Boolean} - true|false if the value is set
     */
    isset: function(value){
        return typeof value != "undefined";
    },

    /**
     * Parses the query param string on the window url
     *
     * @returns {object} - a map of any params found
     */
    parseQueryParams: function(){
        var cParams = {};

        var sQueryString = window.location.search;

        if (sQueryString)
        {
            var aQueryParams = sQueryString.substring(1).split("&");

            aQueryParams.forEach(function(sQueryParam){
                var aParts = sQueryParam.split("=");
                cParams[aParts[0]] = aParts[1];
            });
        }

        return cParams;
    },

    /**
     * Extend an object with other objects
     * Thanks to
     *      stackoverflow http://stackoverflow.com/questions/9399365/deep-extend-like-jquerys-for-nodejs
     *      jQuery https://github.com/jquery/jquery/blob/master/src/core.js
     *
     * @returns {object} - the extended target
     */
    extend: function(){
        var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
                i = 1,
                length = arguments.length,
                deep = false,
                toString = Object.prototype.toString,
                hasOwn = Object.prototype.hasOwnProperty,
                push = Array.prototype.push,
                slice = Array.prototype.slice,
                trim = String.prototype.trim,
                indexOf = Array.prototype.indexOf,
                class2type = {
            "[object Boolean]": "boolean",
            "[object Number]": "number",
            "[object String]": "string",
            "[object Function]": "function",
            "[object Array]": "array",
            "[object Date]": "date",
            "[object RegExp]": "regexp",
            "[object Object]": "object"
        },
        jQuery = {
            isFunction: function(obj) {
                return jQuery.type(obj) === "function";
            },
            isArray: Array.isArray ||
                    function(obj) {
                        return jQuery.type(obj) === "array";
                    },
            isWindow: function(obj) {
                return obj != null && obj == obj.window;
            },
            isNumeric: function(obj) {
                return !isNaN(parseFloat(obj)) && isFinite(obj);
            },
            type: function(obj) {
                return obj == null ? String(obj) : class2type[toString.call(obj)] || "object";
            },
            isPlainObject: function(obj) {
                if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
                    return false;
                }
                try {
                    if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
                        return false;
                    };
                } catch (e) {
                    return false;
                }
                var key;
                for (key in obj) {
                }
                return key === undefined || hasOwn.call(obj, key)
            }
        };
        if (typeof target === "boolean") {
            deep = target;
            target = arguments[1] || {};
            i = 2;
        }
        if (typeof target !== "object" && !jQuery.isFunction(target)) {
            target = {}
        }
        if (length === i) {
            target = this;
            --i;
        }
        for (i; i < length; i++) {
            if ((options = arguments[i]) != null) {
                for (name in options) {
                    src = target[name];
                    copy = options[name];
                    if (target === copy) {
                        continue
                    }
                    if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
                        if (copyIsArray) {
                            copyIsArray = false;
                            clone = src && jQuery.isArray(src) ? src : []
                        } else {
                            clone = src && jQuery.isPlainObject(src) ? src : {};
                        }
                        // WARNING: RECURSION
                        target[name] = extend(deep, clone, copy);
                    } else if (copy !== undefined) {
                        target[name] = copy;
                    }
                }
            }
        }
        return target;
    },

    /**
     *
     * @callback AjaxCompleteCallback
     *
     * @param {Error} cErr
     * @param {*} response
     */

    /**
     * Run an ajax request for a resource
     *
     * @param {object} cOptions
     * @param {string} cOptions.src - the url to load
     * @param {AjaxCompleteCallback} cOptions.onComplete - callback on success or failure
     * @param {string} cOptions.type - GET | POST for request type
     *
     */
    ajaxLoad: function(cOptions){
        var cDefaults = {
            src: null,
            onComplete: function(){},
            type: "GET",
            dataType: "json"
        };

        cOptions = extend({}, cDefaults, cOptions);

        if (cOptions.src)
        {
            var cRequest = new XMLHttpRequest();

            cRequest.onreadystatechange = function(){
                if (cRequest.readyState == 4)
                {
                    if (cRequest.status == 200)
                    {
                        var value = cRequest.response;

                        switch (cOptions.dataType)
                        {
                            case "json":
                                try
                                {
                                    cOptions.onComplete(null, JSON.parse(value));
                                }
                                catch (e)
                                {
                                    cOptions.onComplete(e);
                                }

                                break
                            default:
                                cOptions.onComplete(null, value);
                                break;
                        }
                    }
                    else
                    {
                        var cError = new Error("Failed GET request with status " + cRequest.status + " at url: " + cOptions.src);
                        cOptions.onComplete(cError);
                    }
                }
            };

            cRequest.open("GET", cOptions.src, true);
            cRequest.send();
        }
        else
        {
            cOptions.onComplete(new Error("Missing source parameter"));
        }
    },

    inherits: function(child, parent){
        function Inherited(){};
        Inherited.prototype = parent.prototype;

        child.prototype = new Inherited();
        child.prototype.constructor = child;

        for (var sKey in parent.prototype)
        {
            if (sKey.indexOf("_super") == -1)
            {
                Inherited.prototype["_super" + sKey] = (function(sKey){
                    return function(){
                        return parent.prototype[sKey].apply(child, arguments);
                    };
                })(sKey);
            }
        }
    },

    mixin: function(cObj, cMixin){
        for (var sKey in cMixin)
        {
            cObj.prototype[sKey] = cMixin[sKey];
        }
    }
};

//# sourceURL=engine/base/base.js<|endoftext|>


<commit_before>import * as calculate from '../utils/calculate'
import * as getFromCSV from './scriptUtils/getFromCSV'
import assert from 'assert'
import R from 'ramda'
import readCSV from '../service/readCSV'
import jsonfile from 'jsonfile'

const getProperties = (ev) => ({
  year: getFromCSV.getYear(ev),
  term: getFromCSV.getTerm(ev),
  course: getFromCSV.getCourse(ev),
  section: getFromCSV.getSection(ev),
  courseName: getFromCSV.getCourseName(ev),
  courseLevel: getFromCSV.getCourseLevel(ev),
  dept: getFromCSV.getDept(ev),
  instructorName: getFromCSV.getInstructorName(ev),
  PUID: getFromCSV.getPUID(ev),
  gender: getFromCSV.getGender(ev)
})

// filter out any rows in the csv with all 0 UMI ratings
const filterAll0s = (csv) => csv.filter(ev => {
  if (getFromCSV.getUMI1(ev) === 0 &&
    getFromCSV.getUMI2(ev) === 0 &&
    getFromCSV.getUMI3(ev) === 0 &&
    getFromCSV.getUMI4(ev) === 0 &&
    getFromCSV.getUMI5(ev) === 0 &&
    getFromCSV.getUMI6(ev) === 0) return false
  else return true
})

const createCourseObj = (csv) => {
  const filteredCSV = filterAll0s(csv)
  return filteredCSV.reduce((acc, ev) => {
    const { year, term, course, section, courseName, courseLevel, dept, instructorName, PUID, gender } = getProperties(ev)

    const uniqSectionInTerm = x => (x.year === year &&
      x.course === course &&
      x.term === term &&
      x.section === section &&
      x.instructorName === instructorName)

    if (acc.some(x => uniqSectionInTerm(x))) {
      const index = acc.findIndex(x => uniqSectionInTerm(x))

      for (let i = 1; i <= 6; i++) {
        let getUMI
        let UMI = 'UMI' + i
        if (i === 1) getUMI = getFromCSV.getUMI1(ev)
        if (i === 2) getUMI = getFromCSV.getUMI2(ev)
        if (i === 3) getUMI = getFromCSV.getUMI3(ev)
        if (i === 4) getUMI = getFromCSV.getUMI4(ev)
        if (i === 5) getUMI = getFromCSV.getUMI5(ev)
        if (i === 6) getUMI = getFromCSV.getUMI6(ev)
        if (typeof (acc[index][UMI].count[getUMI]) === 'undefined') {
          acc[index][UMI].count = { ...acc[index][UMI].count, [getUMI]: 1 }
        } else acc[index][UMI].count[getUMI] = acc[index][UMI].count[getUMI] + 1
      }
      acc[index].gender[gender] = acc[index].gender[gender] + 1
      return acc
    } else {
      acc.push({
        year,
        term,
        course,
        section,
        courseName,
        courseLevel,
        dept,
        instructorName,
        PUID,
        gender: {
          Female: (gender === 'Female') ? 1 : 0,
          Male: (gender === 'Male') ? 1 : 0
        },
        UMI1: { count: { [String(getFromCSV.getUMI1(ev))]: 1 } },
        UMI2: { count: { [String(getFromCSV.getUMI2(ev))]: 1 } },
        UMI3: { count: { [String(getFromCSV.getUMI3(ev))]: 1 } },
        UMI4: { count: { [String(getFromCSV.getUMI4(ev))]: 1 } },
        UMI5: { count: { [String(getFromCSV.getUMI5(ev))]: 1 } },
        UMI6: { count: { [String(getFromCSV.getUMI6(ev))]: 1 } }
      })
      return acc
    }
  }, [])
}

const removeIncorrectCounts = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    const countObj = courseObj[UMI].count
    const newObj = R.pick(['1', '2', '3', '4', '5'], countObj)
    courseObj[UMI].count = newObj
  }
  return courseObj
}

const insertDispersionIndex = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].dispersionIndex = calculate.dispersionIndex(courseObj[UMI].count)
  }
  return courseObj
}

const insertAvg = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].average = calculate.umiAvg(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentFav = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].percentFavourable = calculate.percentFavourable(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentileRanking = (courseObjs) => {
  let sortedByUMI
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    sortedByUMI = courseObjs.sort((a, b) => a[UMI].average - b[UMI].average)
    sortedByUMI.map((course) => {
      const filteredByTermAndYear = sortedByUMI.filter(x => x.year === course.year && x.term === course.term)
      const filteredByTermYearAndDept = filteredByTermAndYear.filter(x => x.dept === course.dept)
      course[UMI].percentileRankingByFaculty = calculate.percentileRankingOfCourse(course, UMI, filteredByTermAndYear)
      course[UMI].percentileRankingByDept = calculate.percentileRankingOfCourse(course, UMI, filteredByTermYearAndDept)
    })
  }
  return sortedByUMI
}

// errorChecks
const errorCheck = (courseObjs) => {
  // check to make sure enrolments are all there:
  const courseObjMissingEnrolment = courseObjs.filter(course => !course.hasOwnProperty('enrolment'))
  if (courseObjMissingEnrolment.length !== 0) {
    console.log(courseObjMissingEnrolment)
    throw new Error('Some courses are missing enrolment data')
  }

  const courseObjWithFalsyValues = courseObjs.filter(course => {
    if (!course.year && !course.term && !course.section && !course.courseName && !course.courseLevel && !course.dept && !course.instructorName && !course.PUID) return true
    else return false
  })
  if (courseObjWithFalsyValues.length !== 0) {
    console.log(courseObjWithFalsyValues)
    throw new Error('Some courses have null values')
  }

  const courseObjWithFalsyUMIValues = []
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObjWithFalsyUMIValues.push(courseObjs.filter(course => {
      if (!course[UMI].dispersionIndex && course[UMI].dispersionIndex !== 0) return true
      if (!course[UMI].average && course[UMI].average !== 0) return true
      if (!course[UMI].percentFavourable && course[UMI].percentFavourable !== 0) return true
      if (!course[UMI].percentileRankingByFaculty && course[UMI].percentileRankingByFaculty !== 0) return true
      if (!course[UMI].percentileRankingByDept && course[UMI].percentileRankingByDept !== 0) return true
      else return false
    }))
  }
  if (R.flatten(courseObjWithFalsyUMIValues).length !== 0) {
    // console.log(JSON.stringify(courseObjWithFalsyUMIValues, null, 2))
    // throw new Error('Some courses have UMI values that are invalid (dispersionIndex, average, etc)')
  }
  return true
}

const outputAggregatedData = (cb) => {
  readCSV('../scripts/source/rawDataAll.csv', (csv) => {
    let courseObjs = createCourseObj(csv)

    courseObjs.map(courseObj => R.pipe(
      x => removeIncorrectCounts(x),
      x => insertDispersionIndex(x),
      x => insertAvg(x),
      x => insertPercentFav(x)
    )(courseObj))

    courseObjs = insertPercentileRanking(courseObjs)

    // this adds in the enrolment data from another CSV
    readCSV('../scripts/source/course_eval_enrollments-2009-2017SA.csv', (csv) => {
      csv.map(enrolmentCourse => {
        const { enrolmentCourseName, enrolmentCourseID, enrolmentSection, enrolmentYear, enrolmentTerm, enrolment } =
          {
            enrolmentCourseName: enrolmentCourse.crsname,
            enrolmentCourseID: getFromCSV.getEnrolmentCourseNumber(enrolmentCourse.crsnum),
            enrolmentSection: getFromCSV.getEnrolmentSection(enrolmentCourse.section),
            enrolmentYear: getFromCSV.getEnrolmentYear(enrolmentCourse.period),
            enrolmentTerm: getFromCSV.getEnrolmentTerm(enrolmentCourse.period),
            enrolment: enrolmentCourse.no_enrolled
          }

        courseObjs.map(course => {
          const { courseName, courseID, section, year, term } =
            {
              courseName: course.courseName,
              courseID: course.course,
              section: course.section,
              year: course.year,
              term: course.term
            }
          if (courseName === enrolmentCourseName &&
            courseID === enrolmentCourseID &&
            section === enrolmentSection &&
            year === enrolmentYear &&
            term === enrolmentTerm) {
            course.enrolment = enrolment
            const responses = course.gender.Female + course.gender.Male
            const responseRate = calculate.toTwoDecimal(responses / enrolment)
            course.responseRate = responseRate
            course.meetsMin = calculate.meetsMinimum(enrolment, responseRate)
          }
        })
      })
      if (errorCheck(courseObjs)) {
        const file = './output/aggregatedData.json'
        jsonfile.writeFile(file, courseObjs, (err) => {
          assert.equal(err, null)
          cb()
        })
      }
    })
  })
}

export {
  createCourseObj,
  insertDispersionIndex,
  insertAvg,
  insertPercentFav,
  insertPercentileRanking,
  removeIncorrectCounts,
  outputAggregatedData
}
<commit_msg>changed styling<commit_after>import * as calculate from '../utils/calculate'
import * as getFromCSV from './scriptUtils/getFromCSV'
import assert from 'assert'
import R from 'ramda'
import readCSV from '../service/readCSV'
import jsonfile from 'jsonfile'

const getProperties = ev => ({
  year: getFromCSV.getYear(ev),
  term: getFromCSV.getTerm(ev),
  course: getFromCSV.getCourse(ev),
  section: getFromCSV.getSection(ev),
  courseName: getFromCSV.getCourseName(ev),
  courseLevel: getFromCSV.getCourseLevel(ev),
  dept: getFromCSV.getDept(ev),
  instructorName: getFromCSV.getInstructorName(ev),
  PUID: getFromCSV.getPUID(ev),
  gender: getFromCSV.getGender(ev)
})

// filter out any rows in the csv with all 0 UMI ratings
const filterAll0s = csv => csv.filter(ev => {
  if (getFromCSV.getUMI1(ev) === 0 &&
    getFromCSV.getUMI2(ev) === 0 &&
    getFromCSV.getUMI3(ev) === 0 &&
    getFromCSV.getUMI4(ev) === 0 &&
    getFromCSV.getUMI5(ev) === 0 &&
    getFromCSV.getUMI6(ev) === 0) return false
  else return true
})

const createCourseObj = csv => {
  const filteredCSV = filterAll0s(csv)
  return filteredCSV.reduce((acc, ev) => {
    const { year, term, course, section, courseName, courseLevel, dept, instructorName, PUID, gender } = getProperties(ev)

    const uniqSectionInTerm = x => (x.year === year &&
      x.course === course &&
      x.term === term &&
      x.section === section &&
      x.instructorName === instructorName)

    if (acc.some(x => uniqSectionInTerm(x))) {
      const index = acc.findIndex(x => uniqSectionInTerm(x))

      for (let i = 1; i <= 6; i++) {
        let getUMI
        let UMI = 'UMI' + i
        if (i === 1) getUMI = getFromCSV.getUMI1(ev)
        if (i === 2) getUMI = getFromCSV.getUMI2(ev)
        if (i === 3) getUMI = getFromCSV.getUMI3(ev)
        if (i === 4) getUMI = getFromCSV.getUMI4(ev)
        if (i === 5) getUMI = getFromCSV.getUMI5(ev)
        if (i === 6) getUMI = getFromCSV.getUMI6(ev)
        if (typeof (acc[index][UMI].count[getUMI]) === 'undefined') {
          acc[index][UMI].count = { ...acc[index][UMI].count, [getUMI]: 1 }
        } else acc[index][UMI].count[getUMI] = acc[index][UMI].count[getUMI] + 1
      }
      acc[index].gender[gender] = acc[index].gender[gender] + 1
      return acc
    } else {
      acc.push({
        year,
        term,
        course,
        section,
        courseName,
        courseLevel,
        dept,
        instructorName,
        PUID,
        gender: {
          Female: (gender === 'Female') ? 1 : 0,
          Male: (gender === 'Male') ? 1 : 0
        },
        UMI1: { count: { [String(getFromCSV.getUMI1(ev))]: 1 } },
        UMI2: { count: { [String(getFromCSV.getUMI2(ev))]: 1 } },
        UMI3: { count: { [String(getFromCSV.getUMI3(ev))]: 1 } },
        UMI4: { count: { [String(getFromCSV.getUMI4(ev))]: 1 } },
        UMI5: { count: { [String(getFromCSV.getUMI5(ev))]: 1 } },
        UMI6: { count: { [String(getFromCSV.getUMI6(ev))]: 1 } }
      })
      return acc
    }
  }, [])
}

const removeIncorrectCounts = courseObj => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    const countObj = courseObj[UMI].count
    const newObj = R.pick(['1', '2', '3', '4', '5'], countObj)
    courseObj[UMI].count = newObj
  }
  return courseObj
}

const insertDispersionIndex = courseObj => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].dispersionIndex = calculate.dispersionIndex(courseObj[UMI].count)
  }
  return courseObj
}

const insertAvg = courseObj => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].average = calculate.umiAvg(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentFav = courseObj => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].percentFavourable = calculate.percentFavourable(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentileRanking = courseObjs => {
  let sortedByUMI
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    sortedByUMI = courseObjs.sort((a, b) => a[UMI].average - b[UMI].average)
    sortedByUMI.map((course) => {
      const filteredByTermAndYear = sortedByUMI.filter(x => x.year === course.year && x.term === course.term)
      const filteredByTermYearAndDept = filteredByTermAndYear.filter(x => x.dept === course.dept)
      course[UMI].percentileRankingByFaculty = calculate.percentileRankingOfCourse(course, UMI, filteredByTermAndYear)
      course[UMI].percentileRankingByDept = calculate.percentileRankingOfCourse(course, UMI, filteredByTermYearAndDept)
    })
  }
  return sortedByUMI
}

// errorChecks
const errorCheck = courseObjs => {
  // check to make sure enrolments are all there:
  const courseObjMissingEnrolment = courseObjs.filter(course => !course.hasOwnProperty('enrolment'))
  if (courseObjMissingEnrolment.length !== 0) {
    console.log(courseObjMissingEnrolment)
    throw new Error('Some courses are missing enrolment data')
  }

  const courseObjWithFalsyValues = courseObjs.filter(course => {
    if (!course.year && !course.term && !course.section && !course.courseName && !course.courseLevel && !course.dept && !course.instructorName && !course.PUID) return true
    else return false
  })
  if (courseObjWithFalsyValues.length !== 0) {
    console.log(courseObjWithFalsyValues)
    throw new Error('Some courses have null values')
  }

  const courseObjWithFalsyUMIValues = []
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObjWithFalsyUMIValues.push(courseObjs.filter(course => {
      if (!course[UMI].dispersionIndex && course[UMI].dispersionIndex !== 0) return true
      if (!course[UMI].average && course[UMI].average !== 0) return true
      if (!course[UMI].percentFavourable && course[UMI].percentFavourable !== 0) return true
      if (!course[UMI].percentileRankingByFaculty && course[UMI].percentileRankingByFaculty !== 0) return true
      if (!course[UMI].percentileRankingByDept && course[UMI].percentileRankingByDept !== 0) return true
      else return false
    }))
  }
  if (R.flatten(courseObjWithFalsyUMIValues).length !== 0) {
    // console.log(JSON.stringify(courseObjWithFalsyUMIValues, null, 2))
    // throw new Error('Some courses have UMI values that are invalid (dispersionIndex, average, etc)')
  }
  return true
}

const outputAggregatedData = cb => {
  readCSV('../scripts/source/rawDataAll.csv', (csv) => {
    let courseObjs = createCourseObj(csv)

    courseObjs.map(courseObj => R.pipe(
      x => removeIncorrectCounts(x),
      x => insertDispersionIndex(x),
      x => insertAvg(x),
      x => insertPercentFav(x)
    )(courseObj))

    courseObjs = insertPercentileRanking(courseObjs)

    // this adds in the enrolment data from another CSV
    readCSV('../scripts/source/course_eval_enrollments-2009-2017SA.csv', csv => {
      csv.map(enrolmentCourse => {
        const { enrolmentCourseName, enrolmentCourseID, enrolmentSection, enrolmentYear, enrolmentTerm, enrolment } =
          {
            enrolmentCourseName: enrolmentCourse.crsname,
            enrolmentCourseID: getFromCSV.getEnrolmentCourseNumber(enrolmentCourse.crsnum),
            enrolmentSection: getFromCSV.getEnrolmentSection(enrolmentCourse.section),
            enrolmentYear: getFromCSV.getEnrolmentYear(enrolmentCourse.period),
            enrolmentTerm: getFromCSV.getEnrolmentTerm(enrolmentCourse.period),
            enrolment: enrolmentCourse.no_enrolled
          }

        courseObjs.map(course => {
          const { courseName, courseID, section, year, term } =
            {
              courseName: course.courseName,
              courseID: course.course,
              section: course.section,
              year: course.year,
              term: course.term
            }
          if (courseName === enrolmentCourseName &&
            courseID === enrolmentCourseID &&
            section === enrolmentSection &&
            year === enrolmentYear &&
            term === enrolmentTerm) {
            course.enrolment = enrolment
            const responses = course.gender.Female + course.gender.Male
            const responseRate = calculate.toTwoDecimal(responses / enrolment)
            course.responseRate = responseRate
            course.meetsMin = calculate.meetsMinimum(enrolment, responseRate)
          }
        })
      })
      if (errorCheck(courseObjs)) {
        const file = './output/aggregatedData.json'
        jsonfile.writeFile(file, courseObjs, (err) => {
          assert.equal(err, null)
          cb()
        })
      }
    })
  })
}

export {
  createCourseObj,
  insertDispersionIndex,
  insertAvg,
  insertPercentFav,
  insertPercentileRanking,
  removeIncorrectCounts,
  outputAggregatedData
}
<|endoftext|>


<commit_before>/*
**  Microkernel -- Microkernel for Server Applications
**  Copyright (c) 2015 Ralf S. Engelschall <rse@engelschall.com>
**
**  Permission is hereby granted, free of charge, to any person obtaining
**  a copy of this software and associated documentation files (the
**  "Software"), to deal in the Software without restriction, including
**  without limitation the rights to use, copy, modify, merge, publish,
**  distribute, sublicense, and/or sell copies of the Software, and to
**  permit persons to whom the Software is furnished to do so, subject to
**  the following conditions:
**
**  The above copyright notice and this permission notice shall be included
**  in all copies or substantial portions of the Software.
**
**  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
**  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
**  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
**  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
**  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
**  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
**  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import aggregation from "aggregation/es6"

import Version     from "./microkernel-1-version"
import Manifest    from "./microkernel-2-manifest"
import Loader      from "./microkernel-3-loader"
import State       from "./microkernel-4-state"
import Hook        from "./microkernel-5-hook"
import Event       from "./microkernel-6-event"
import Service     from "./microkernel-7-service"
import Resource    from "./microkernel-8-resource"

/*  the API class  */
export default class Microkernel extends
    aggregation(Version, Manifest, Loader, State,
                Hook, Event, Service, Resource) {
    /*  initialize the microkernel instance  */
    constructor () {
        super()
    }
}

<commit_msg>ensure that the result is the same even we are now in a newer Babel world<commit_after>/*
**  Microkernel -- Microkernel for Server Applications
**  Copyright (c) 2015 Ralf S. Engelschall <rse@engelschall.com>
**
**  Permission is hereby granted, free of charge, to any person obtaining
**  a copy of this software and associated documentation files (the
**  "Software"), to deal in the Software without restriction, including
**  without limitation the rights to use, copy, modify, merge, publish,
**  distribute, sublicense, and/or sell copies of the Software, and to
**  permit persons to whom the Software is furnished to do so, subject to
**  the following conditions:
**
**  The above copyright notice and this permission notice shall be included
**  in all copies or substantial portions of the Software.
**
**  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
**  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
**  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
**  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
**  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
**  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
**  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import aggregation from "aggregation/es6"

import Version     from "./microkernel-1-version"
import Manifest    from "./microkernel-2-manifest"
import Loader      from "./microkernel-3-loader"
import State       from "./microkernel-4-state"
import Hook        from "./microkernel-5-hook"
import Event       from "./microkernel-6-event"
import Service     from "./microkernel-7-service"
import Resource    from "./microkernel-8-resource"

/*  the API class  */
class Microkernel extends
    aggregation(Version, Manifest, Loader, State,
                Hook, Event, Service, Resource) {
    /*  initialize the microkernel instance  */
    constructor () {
        super()
    }
}

/*  export the traditional way for interoperability reasons
    (as Babel would export an object with a 'default' field)  */
module.exports = Microkernel

<|endoftext|>


<commit_before>'use strict';

var async = require('async');

var LocationsCreateController = function ($scope, $routeParams, apiService, authService, $q, $location, mapService,
                                          responseHandler, locationFormDefaults, regionService, GeolocationService) {
    var changeTimer, lockUpdate;
    $scope.location = {};
    $scope.assets = {
        file: null
    };
    $scope.marker = {
        lat: null,
        lng: null
    };
    $scope.selectedIndex = 0;
    $scope._sys = locationFormDefaults;

    GeolocationService.getCurrentPosition().then(
        function(position) { //
            console.log('Position',position);
            $scope.marker.lat = position.coords.latitude;
            $scope.marker.lng = position.coords.longitude;
            $scope.updateLocation({ 'lat' : position.coords.latitude , 'lng': position.coords.longitude});
        },
        function(errorCode) {
            if(errorCode === false) {
                alert('GeoLocation is not supported by browser.');
            }
            else if(errorCode == 1) {
                alert('User either denied GeoLocation or waited for long to respond.');
            }
        }
    );

    /*
        Update the map to a new geolocation
        Also retrieves the new address
    */
    $scope.updateLocation = function (latlon) {
        $scope.marker = latlon;

        mapService.reverseGeoCode(latlon.lat,latlon.lng,function(err, result){
            if (err) {
                throw err;
            }
            lockUpdate = true;
            if (!result.error) {
                console.log('updateLocation',result.data);

                var address = mapService.createAddressFromGeo(result.data.address);
                if (address) {
                    $scope.location.street = address.street;
                    $scope.location.city = address.city;
                    $scope.location.postcode = address.postcode;
                    $scope.location.country = address.country;
                }
            }
        });
        $scope.showLatLng = false;
    };

    /*
        Watches the address fields for updates
        Updates the map on address changes
     */
    $scope.$watchGroup(['location.street', 'location.city', 'location.postcode'], function (newValues, oldValues) {
        var changed = false,
            keys = Object.keys(newValues);

        if (lockUpdate) {
            lockUpdate = false;
            return;
        }

        for (var i = 0; i < keys.length; i += 1) {
            if (newValues[keys[i]] !== oldValues[keys[i]]) {
                changed = true;
            }
        }

        if (changed) {
            if (changeTimer) {
                window.clearTimeout(changeTimer);
                changeTimer = null;
            }
            changeTimer = window.setTimeout(function () {
                mapService.geoCode({
                    street: $scope.location.street,
                    city: $scope.location.city,
                    postcode: $scope.location.postcode,
                    country: $scope.location.country
                }, function (err, result) {
                    console.log('found locations',result.data);
                    if (Array.isArray(result.data) && result.data.length > 0) {
                        $scope.marker.lat = result.data[0].lat;
                        $scope.marker.lng = result.data[0].lon;
                        $scope.location.display_name = result.data[0].display_name || "";
                        $scope.center = [result.data[0].lat,result.data[0].lon];

                    }
                });
            },1000);
        }
    });

    /*
        Submit the new location
     */
    $scope.submit = function () {
        var deferred = $q.defer(),
            payload = $scope.location;
        $scope.promise = deferred.promise;
        payload.lonlat = [$scope.marker.lng, $scope.marker.lat];
        var isUpdate = false;
        async.waterfall([
            function (cb) {
                /*
                    Fetch the nearest region
                 */
                // TODO: this must be limited through region admins!
                apiService('regions?lat=' + $scope.marker.lat + '&lon=' + $scope.marker.lng).actions.all(cb);
            },
            function (region, cb) {
                if (region && region.length > 0) {
                    if (!payload.hasOwnProperty('region_uuid')) {
                        payload.region_uuid = region[0].uuid;
                    }
                    /*
                        Are we updating or creating?
                     */
                    if (payload.uuid) {
                        isUpdate = true;
                        apiService('locations').actions.update(payload.uuid, payload, cb);
                    } else {
                        apiService('locations').actions.create(payload, cb);
                    }
                } else {
                    cb(new Error('errors.regions.fetch_failed'),null);
                }
            },
            function (location, cb) {
                if (!isUpdate) {
                    $scope.location.uuid = location.uuid;
                }
                $scope.location.slug = location.slug;
                /*
                    Upload the associated photos
                 */
                if ($scope.files && $scope.files.length) {
                    async.mapSeries($scope.files, function (file, cb) {
                        apiService('photos').actions.upload({
                            file: file,
                            fields: {location_uuid: location.uuid}
                        }, cb);
                    }, cb);
                } else {
                    cb();
                }
            }
        ], function (err) {
            var msgs = {
                success: $routeParams.uuid ? 'messages.locations.update_success' : 'messages.locations.create_success'
            };
            if (responseHandler.handleResponse(err, deferred, msgs)) {
                $location.path('/' + ($scope.location.region_slug || $scope.location.region_uuid) + '/' +
                    $scope.location.slug);
            }
        });
    };

    /*
        Load an existing location to update
     */
    if ($routeParams.uuid) {
        var deferred = $q.defer();
        $scope.promise = deferred.promise;
        async.waterfall([
            function (cb) {
                apiService('locations').actions.find($routeParams.uuid, cb);
            },
            function (location, cb) {
                if (!location.hasOwnProperty('hidden')) {
                    location.hidden = false;
                }
                $scope.location = location;
                $scope.marker = {
                    lng: location.lonlat[0],
                    lat: location.lonlat[1]
                };
                $scope.formTitle = 'Edit "' + location.title + '"';
                apiService('locations/' + location.uuid + '/photos').actions.all(cb);
            },
            function (photos, cb) {
                $scope.photos =  photos.result || photos;
                cb();
            },
            function (cb) {
                regionService.setCurrentRegion($scope.location.region_uuid, cb);
            },
            function (cb) {
                $scope.currentRegion = regionService.currentRegion.title;
                $scope.isAdmin = authService.hasScopes(['admin', 'region-' + $scope.location.region_uuid]);
                cb();
            }
        ], function (err) {
            responseHandler.handleResponse(err, deferred);
        });
    }
};

LocationsCreateController.$inject = ['$scope', '$routeParams', 'apiService', 'authService', '$q', '$location',
    'mapService', 'responseHandler', 'locationFormDefaults', 'regionService','GeolocationService'];

module.exports = LocationsCreateController;<commit_msg>Fix broken upload; dis api change<commit_after>'use strict';

var async = require('async');

var LocationsCreateController = function ($scope, $routeParams, apiService, authService, $q, $location, mapService,
                                          responseHandler, locationFormDefaults, regionService, GeolocationService) {
    var changeTimer, lockUpdate;
    $scope.location = {};
    $scope.assets = {
        file: null
    };
    $scope.marker = {
        lat: null,
        lng: null
    };
    $scope.selectedIndex = 0;
    $scope._sys = locationFormDefaults;

    GeolocationService.getCurrentPosition().then(
        function(position) { //
            console.log('Position',position);
            $scope.marker.lat = position.coords.latitude;
            $scope.marker.lng = position.coords.longitude;
            $scope.updateLocation({ 'lat' : position.coords.latitude , 'lng': position.coords.longitude});
        },
        function(errorCode) {
            if(errorCode === false) {
                alert('GeoLocation is not supported by browser.');
            }
            else if(errorCode == 1) {
                alert('User either denied GeoLocation or waited for long to respond.');
            }
        }
    );

    /*
        Update the map to a new geolocation
        Also retrieves the new address
    */
    $scope.updateLocation = function (latlon) {
        $scope.marker = latlon;

        mapService.reverseGeoCode(latlon.lat,latlon.lng,function(err, result){
            if (err) {
                throw err;
            }
            lockUpdate = true;
            if (!result.error) {
                console.log('updateLocation',result.data);

                var address = mapService.createAddressFromGeo(result.data.address);
                if (address) {
                    $scope.location.street = address.street;
                    $scope.location.city = address.city;
                    $scope.location.postcode = address.postcode;
                    $scope.location.country = address.country;
                }
            }
        });
        $scope.showLatLng = false;
    };

    /*
        Watches the address fields for updates
        Updates the map on address changes
     */
    $scope.$watchGroup(['location.street', 'location.city', 'location.postcode'], function (newValues, oldValues) {
        var changed = false,
            keys = Object.keys(newValues);

        if (lockUpdate) {
            lockUpdate = false;
            return;
        }

        for (var i = 0; i < keys.length; i += 1) {
            if (newValues[keys[i]] !== oldValues[keys[i]]) {
                changed = true;
            }
        }

        if (changed) {
            if (changeTimer) {
                window.clearTimeout(changeTimer);
                changeTimer = null;
            }
            changeTimer = window.setTimeout(function () {
                mapService.geoCode({
                    street: $scope.location.street,
                    city: $scope.location.city,
                    postcode: $scope.location.postcode,
                    country: $scope.location.country
                }, function (err, result) {
                    console.log('found locations',result.data);
                    if (Array.isArray(result.data) && result.data.length > 0) {
                        $scope.marker.lat = result.data[0].lat;
                        $scope.marker.lng = result.data[0].lon;
                        $scope.location.display_name = result.data[0].display_name || "";
                        $scope.center = [result.data[0].lat,result.data[0].lon];

                    }
                });
            },1000);
        }
    });

    /*
        Submit the new location
     */
    $scope.submit = function () {
        var deferred = $q.defer(),
            payload = $scope.location;
        $scope.promise = deferred.promise;
        payload.lonlat = [$scope.marker.lng, $scope.marker.lat];
        var isUpdate = false;
        async.waterfall([
            function (cb) {
                /*
                    Fetch the nearest region
                 */
                // TODO: this must be limited through region admins!
                apiService('regions?lat=' + $scope.marker.lat + '&lon=' + $scope.marker.lng).actions.all(cb);
            },
            function (region, cb) {
                region = region.results || region;
                if (region && region.length > 0) {
                    if (!payload.hasOwnProperty('region_uuid')) {
                        payload.region_uuid = region[0].uuid;
                    }
                    /*
                        Are we updating or creating?
                     */
                    if (payload.uuid) {
                        isUpdate = true;
                        apiService('locations').actions.update(payload.uuid, payload, cb);
                    } else {
                        apiService('locations').actions.create(payload, cb);
                    }
                } else {
                    cb(new Error('errors.regions.fetch_failed'),null);
                }
            },
            function (location, cb) {
                if (!isUpdate) {
                    $scope.location.uuid = location.uuid;
                }
                $scope.location.slug = location.slug;
                /*
                    Upload the associated photos
                 */
                if ($scope.files && $scope.files.length) {
                    async.mapSeries($scope.files, function (file, cb) {
                        apiService('photos').actions.upload({
                            file: file,
                            fields: {location_uuid: location.uuid}
                        }, cb);
                    }, cb);
                } else {
                    cb();
                }
            }
        ], function (err) {
            var msgs = {
                success: $routeParams.uuid ? 'messages.locations.update_success' : 'messages.locations.create_success'
            };
            if (responseHandler.handleResponse(err, deferred, msgs)) {
                $location.path('/' + ($scope.location.region_slug || $scope.location.region_uuid) + '/' +
                    $scope.location.slug);
            }
        });
    };

    /*
        Load an existing location to update
     */
    if ($routeParams.uuid) {
        var deferred = $q.defer();
        $scope.promise = deferred.promise;
        async.waterfall([
            function (cb) {
                apiService('locations').actions.find($routeParams.uuid, cb);
            },
            function (location, cb) {
                if (!location.hasOwnProperty('hidden')) {
                    location.hidden = false;
                }
                $scope.location = location;
                $scope.marker = {
                    lng: location.lonlat[0],
                    lat: location.lonlat[1]
                };
                $scope.formTitle = 'Edit "' + location.title + '"';
                apiService('locations/' + location.uuid + '/photos').actions.all(cb);
            },
            function (photos, cb) {
                $scope.photos =  photos.result || photos;
                cb();
            },
            function (cb) {
                regionService.setCurrentRegion($scope.location.region_uuid, cb);
            },
            function (cb) {
                $scope.currentRegion = regionService.currentRegion.title;
                $scope.isAdmin = authService.hasScopes(['admin', 'region-' + $scope.location.region_uuid]);
                cb();
            }
        ], function (err) {
            responseHandler.handleResponse(err, deferred);
        });
    }
};

LocationsCreateController.$inject = ['$scope', '$routeParams', 'apiService', 'authService', '$q', '$location',
    'mapService', 'responseHandler', 'locationFormDefaults', 'regionService','GeolocationService'];

module.exports = LocationsCreateController;<|endoftext|>