<commit_before>import { createSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
import uniqBy from 'lodash/uniqBy';
import sumBy from 'lodash/sumBy';
import { sortByKey, getColorPalette } from 'utils/data';
import { format } from 'd3-format';

// get list data
const getData = state => state.data || null;
const getSettings = state => state.settings || null;
const getOptions = state => state.options || null;
const getIndicator = state => state.indicator || null;
const getLocation = state => state.location || null;
const getLocationsMeta = state => state.meta || null;
const getLocationNames = state => state.locationNames || null;
const getColors = state => state.colors || null;

export const getSortedData = createSelector(
  [getData, getSettings, getLocation, getLocationsMeta],
  (data, settings, location, meta) => {
    if (!data || isEmpty(data) || !meta || isEmpty(meta)) return null;
    const dataMapped = [];
    data.forEach(d => {
      const region = meta.find(l => d.id === l.value);
      if (region) {
        dataMapped.push({
          label: (region && region.label) || '',
          extent: d.extent,
          percentage: d.percentage,
          value: settings.unit === 'ha' ? d.extent : d.percentage,
          path: `/country/${location.country}/${
            location.region ? `${location.region}/` : ''
          }${d.id}`
        });
      }
    });
    return sortByKey(uniqBy(dataMapped, 'label'), 'value', true);
  }
);

export const getChartData = createSelector(
  [getSortedData, getColors],
  (data, colors) => {
    if (!data || !data.length) return null;
    const topRegions = data.length > 10 ? data.slice(0, 10) : data;
    const totalExtent = sumBy(data, 'extent');
    const otherRegions = data.length > 10 ? data.slice(10) : [];
    const othersExtent = otherRegions.length && sumBy(otherRegions, 'extent');
    const colorRange = getColorPalette(
      [colors.darkGreen, colors.nonForest],
      data.length > 10 ? topRegions.length + 1 : data.length
    );
    const topChartData = topRegions.map((d, index) => ({
      ...d,
      percentage: d.extent / totalExtent * 100,
      color: colorRange[index]
    }));
    const otherRegionsData = otherRegions.length
      ? {
        label: 'Other regions',
        percentage: othersExtent ? othersExtent / totalExtent * 100 : 0,
        color: colorRange[topRegions.length]
      }
      : {};

    return [...topChartData, otherRegionsData];
  }
);

export const getSentence = createSelector(
  [
    getSortedData,
    getSettings,
    getOptions,
    getLocation,
    getIndicator,
    getLocationNames
  ],
  (data, settings, options, location, indicator, locationNames) => {
    if (!data || !options || !indicator || !locationNames) return '';
    const { extentYear, threshold } = settings;
    const totalExtent = sumBy(data, 'extent');
    const currentLocation =
      locationNames && locationNames.current && locationNames.current.label;
    const topRegion = data.length && data[0];
    const avgExtentPercentage = sumBy(data, 'percentage') / data.length;
    const avgExtent = sumBy(data, 'extent');
    let percentileExtent = 0;
    let percentileLength = 0;
    let sentence = '';

    if (indicator.value !== 'gadm28') {
      sentence += `For ${
        indicator.label
      } in ${currentLocation}, `;
    } else {
      sentence += `In <b>${currentLocation}</b>, `;
    }
    while (
      percentileLength < data.length &&
      percentileExtent / totalExtent < 0.5
    ) {
      percentileExtent += data[percentileLength].extent;
      percentileLength += 1;
    }
    const topExtent = percentileExtent / totalExtent * 100;

    if (percentileLength > 1) {
      sentence += `the top <b>${percentileLength}</b> regions represents <b>`;
    } else {
      sentence += `<b>${topRegion.label}</b> represents <b>`;
    }
    if (!location.region) {
      sentence += `more than half (${format('.0f')(topExtent)}%)`;
    } else {
      sentence += `${format('.0f')(topExtent)}%`;
    }
    sentence += `</b> of all tree cover in <b>${extentYear}</b> where tree canopy is greater than <b>${threshold}%</b>. `;
    sentence += `${
      percentileLength > 1 ? `<b>${topRegion.label}</b>` : 'This region'
    } has the largest ${
      settings.unit === '%' ? 'relative' : ''
    } tree cover at `;
    if (topRegion.percentage > 1 && settings.unit === '%') {
      sentence += `${format('.0f')(
        topRegion.percentage
      )}% compared to an average of ${format('.0f')(
        avgExtentPercentage
      )}%.`;
    } else {
      sentence += `${format('.2s')(
        topRegion.extent
      )}ha compared to an average of ${format('.2s')(avgExtent)}ha.`;
    }

    return sentence;
  }
);
<commit_msg>fix av calc<commit_after>import { createSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
import uniqBy from 'lodash/uniqBy';
import sumBy from 'lodash/sumBy';
import { sortByKey, getColorPalette } from 'utils/data';
import { format } from 'd3-format';

// get list data
const getData = state => state.data || null;
const getSettings = state => state.settings || null;
const getOptions = state => state.options || null;
const getIndicator = state => state.indicator || null;
const getLocation = state => state.location || null;
const getLocationsMeta = state => state.meta || null;
const getLocationNames = state => state.locationNames || null;
const getColors = state => state.colors || null;

export const getSortedData = createSelector(
  [getData, getSettings, getLocation, getLocationsMeta],
  (data, settings, location, meta) => {
    if (!data || isEmpty(data) || !meta || isEmpty(meta)) return null;
    const dataMapped = [];
    data.forEach(d => {
      const region = meta.find(l => d.id === l.value);
      if (region) {
        dataMapped.push({
          label: (region && region.label) || '',
          extent: d.extent,
          percentage: d.percentage,
          value: settings.unit === 'ha' ? d.extent : d.percentage,
          path: `/country/${location.country}/${
            location.region ? `${location.region}/` : ''
          }${d.id}`
        });
      }
    });
    return sortByKey(uniqBy(dataMapped, 'label'), 'value', true);
  }
);

export const getChartData = createSelector(
  [getSortedData, getColors],
  (data, colors) => {
    if (!data || !data.length) return null;
    const topRegions = data.length > 10 ? data.slice(0, 10) : data;
    const totalExtent = sumBy(data, 'extent');
    const otherRegions = data.length > 10 ? data.slice(10) : [];
    const othersExtent = otherRegions.length && sumBy(otherRegions, 'extent');
    const colorRange = getColorPalette(
      [colors.darkGreen, colors.nonForest],
      data.length > 10 ? topRegions.length + 1 : data.length
    );
    const topChartData = topRegions.map((d, index) => ({
      ...d,
      percentage: d.extent / totalExtent * 100,
      color: colorRange[index]
    }));
    const otherRegionsData = otherRegions.length
      ? {
        label: 'Other regions',
        percentage: othersExtent ? othersExtent / totalExtent * 100 : 0,
        color: colorRange[topRegions.length]
      }
      : {};

    return [...topChartData, otherRegionsData];
  }
);

export const getSentence = createSelector(
  [
    getSortedData,
    getSettings,
    getOptions,
    getLocation,
    getIndicator,
    getLocationNames
  ],
  (data, settings, options, location, indicator, locationNames) => {
    if (!data || !options || !indicator || !locationNames) return '';
    const { extentYear, threshold } = settings;
    const totalExtent = sumBy(data, 'extent');
    const currentLocation =
      locationNames && locationNames.current && locationNames.current.label;
    const topRegion = data.length && data[0];
    const avgExtentPercentage = sumBy(data, 'percentage') / data.length;
    const avgExtent = sumBy(data, 'extent') / data.length;
    let percentileExtent = 0;
    let percentileLength = 0;
    let sentence = '';

    if (indicator.value !== 'gadm28') {
      sentence += `For ${
        indicator.label
      } in ${currentLocation}, `;
    } else {
      sentence += `In <b>${currentLocation}</b>, `;
    }
    while (
      percentileLength < data.length &&
      percentileExtent / totalExtent < 0.5
    ) {
      percentileExtent += data[percentileLength].extent;
      percentileLength += 1;
    }
    const topExtent = percentileExtent / totalExtent * 100;

    if (percentileLength > 1) {
      sentence += `the top <b>${percentileLength}</b> regions represents <b>`;
    } else {
      sentence += `<b>${topRegion.label}</b> represents <b>`;
    }
    if (!location.region) {
      sentence += `more than half (${format('.0f')(topExtent)}%)`;
    } else {
      sentence += `${format('.0f')(topExtent)}%`;
    }
    sentence += `</b> of all tree cover in <b>${extentYear}</b> where tree canopy is greater than <b>${threshold}%</b>. `;
    sentence += `${
      percentileLength > 1 ? `<b>${topRegion.label}</b>` : 'This region'
    } has the largest ${
      settings.unit === '%' ? 'relative' : ''
    } tree cover at `;
    if (topRegion.percentage > 1 && settings.unit === '%') {
      sentence += `${format('.0f')(
        topRegion.percentage
      )}% compared to an average of ${format('.0f')(
        avgExtentPercentage
      )}%.`;
    } else {
      sentence += `${format('.3s')(
        topRegion.extent
      )}ha compared to an average of ${format('.3s')(avgExtent)}ha.`;
    }

    return sentence;
  }
);
<|endoftext|>


<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2020
 */

import { SUPPLIER_CREDIT_ACTIONS } from '../actions/SupplierCreditActions';
import { UIDatabase } from '../database/index';
import { parsePositiveInteger } from '../utilities';

const mapBatchToObject = batches =>
  batches.map(batch => ({
    returnAmount: 0,
    id: batch.id,
    totalQuantity: batch.totalQuantity,
    itemBatch: batch,
    otherPartyName: batch.otherPartyName,
    batch: batch.batch,
    costPrice: batch.costPrice,
  }));

const initialState = () => ({
  batches: [],
  open: false,
  sortKey: 'otherPartyName',
  isAscending: true,
  item: null,
});

export const SupplierCreditReducer = (state = initialState(), action) => {
  const { type } = action;

  switch (type) {
    case SUPPLIER_CREDIT_ACTIONS.EDIT_RETURN_AMOUNT: {
      const { batches } = state;
      const { payload } = action;
      const { batchId, returnAmount } = payload;

      const rowToEditIndex = batches.findIndex(({ id }) => id === batchId);
      const newRow = {
        ...batches[rowToEditIndex],
        returnAmount: parsePositiveInteger(returnAmount),
      };
      batches[rowToEditIndex] = newRow;

      return { ...state, batches: [...batches] };
    }

    case SUPPLIER_CREDIT_ACTIONS.CREATE_FROM_ITEM: {
      const { payload } = action;
      const { itemId } = payload;

      const batches = UIDatabase.objects('ItemBatch').filtered(
        'item.id == $0 && numberOfPacks > 0',
        itemId
      );

      return {
        ...state,
        open: true,
        batches: mapBatchToObject(batches),
        item: UIDatabase.get('Item', itemId),
      };
    }

    case SUPPLIER_CREDIT_ACTIONS.SORT: {
      const { isAscending, sortKey } = state;
      const { payload } = action;
      const { sortKey: newSortKey } = payload;

      return {
        ...state,
        sortKey: newSortKey,
        isAscending: sortKey === newSortKey ? !isAscending : true,
      };
    }

    case SUPPLIER_CREDIT_ACTIONS.CLOSE: {
      return initialState();
    }

    default:
      return state;
  }
};
<commit_msg>Add title field to state<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2020
 */

import { SUPPLIER_CREDIT_ACTIONS } from '../actions/SupplierCreditActions';
import { UIDatabase } from '../database/index';
import { parsePositiveInteger } from '../utilities';

const mapBatchToObject = batches =>
  batches.map(batch => ({
    returnAmount: 0,
    id: batch.id,
    totalQuantity: batch.totalQuantity,
    itemBatch: batch,
    otherPartyName: batch.otherPartyName,
    batch: batch.batch,
    costPrice: batch.costPrice,
  }));

const initialState = () => ({
  batches: [],
  open: false,
  sortKey: 'otherPartyName',
  isAscending: true,
  item: null,
  title: '',
});

export const SupplierCreditReducer = (state = initialState(), action) => {
  const { type } = action;

  switch (type) {
    case SUPPLIER_CREDIT_ACTIONS.EDIT_RETURN_AMOUNT: {
      const { batches } = state;
      const { payload } = action;
      const { batchId, returnAmount } = payload;

      const rowToEditIndex = batches.findIndex(({ id }) => id === batchId);
      const newRow = {
        ...batches[rowToEditIndex],
        returnAmount: parsePositiveInteger(returnAmount),
      };
      batches[rowToEditIndex] = newRow;

      return { ...state, batches: [...batches] };
    }

    case SUPPLIER_CREDIT_ACTIONS.CREATE_FROM_ITEM: {
      const { payload } = action;
      const { itemId } = payload;

      const batches = UIDatabase.objects('ItemBatch').filtered(
        'item.id == $0 && numberOfPacks > 0',
        itemId
      );

      return {
        ...state,
        open: true,
        batches: mapBatchToObject(batches),
        item: UIDatabase.get('Item', itemId),
        title: `Available Credits for  ${UIDatabase.get('Item', itemId).name}`,
      };
    }

    case SUPPLIER_CREDIT_ACTIONS.CREATE_FROM_INVOICE: {
      const { payload } = action;
      const { invoice } = payload;

      const { serialNumber } = invoice;
      const transactionBatches = invoice.getTransactionBatches(UIDatabase);
      const itemBatches = transactionBatches.map(({ itemBatch }) => itemBatch);

      return {
        ...state,
        open: true,
        batches: mapBatchToObject(itemBatches),
        item: UIDatabase.objects('Item')[0],
        title: `Supplier Credit for Supplier Invoice ${serialNumber}`,
      };
    }

    case SUPPLIER_CREDIT_ACTIONS.SORT: {
      const { isAscending, sortKey } = state;
      const { payload } = action;
      const { sortKey: newSortKey } = payload;

      return {
        ...state,
        sortKey: newSortKey,
        isAscending: sortKey === newSortKey ? !isAscending : true,
      };
    }

    case SUPPLIER_CREDIT_ACTIONS.CLOSE: {
      return initialState();
    }

    default:
      return state;
  }
};
<|endoftext|>


<commit_before>Ext.define('devilry.administrator.studentsmanager.StudentsManager', {
    extend: 'devilry.extjshelpers.studentsmanager.StudentsManager',
    alias: 'widget.administrator_studentsmanager',

    mixins: {
        manageExaminers: 'devilry.administrator.studentsmanager.StudentsManagerManageExaminers',
        createGroups: 'devilry.administrator.studentsmanager.StudentsManagerManageGroups',
        loadRelatedUsers: 'devilry.administrator.studentsmanager.LoadRelatedUsersMixin',
        addDeliveries: 'devilry.administrator.studentsmanager.AddDeliveriesMixin'
    },

    //config: {
        //assignmentgroupPrevApprovedStore: undefined
    //},

    constructor: function(config) {
        this.initConfig(config);
        this.callParent([config]);
    },

    initComponent: function() {
        this.addStudentsButton = Ext.widget('button', {
            text: 'Create groups',
            iconCls: 'icon-add-32',
            scale: 'large',
            menu: [{
                text: Ext.String.format('One group for each student registered in {0}', DevilrySettings.DEVILRY_SYNCSYSTEM),
                listeners: {
                    scope: this,
                    click: this.onOneGroupForEachRelatedStudent
                }
            }, {
                text: 'Manually',
                listeners: {
                    scope: this,
                    click: this.onManuallyCreateUsers
                }
            }]
        });

        this.callParent(arguments);
    },

    getToolbarItems: function() {
        var defaults = this.callParent();
        Ext.Array.insert(defaults, 2, [this.addStudentsButton]);
        return defaults;
    },

    getOnSingleMenuItems: function() {
        var menu = this.callParent();
        //menu.push({
            //text: 'Create dummy delivery',
            //listeners: {
                //scope: this,
                //click: this.onCreateDummyDelivery
            //}
        //});
        menu.push({
            text: 'Change group members',
            iconCls: 'icon-edit-16',
            listeners: {
                scope: this,
                click: this.onChangeGroupMembers
            }
        });
        menu.push({
            text: 'Change group name',
            iconCls: 'icon-edit-16',
            listeners: {
                scope: this,
                click: this.onChangeGroupName
            }
        });
        return menu;
    },

    getOnManyMenuItems: function() {
        var menu = this.callParent();
        menu.push({
            text: 'Mark as delivered in a previous period',
            listeners: {
                scope: this,
                click: this.onPreviouslyPassed
            }
        });
        menu.push({
            text: 'Change examiners',
            menu: [{
                text: 'Replace',
                iconCls: 'icon-edit-16',
                listeners: {
                    scope: this,
                    click: this.onReplaceExaminers
                }
            }, {
                text: 'Add',
                iconCls: 'icon-add-16',
                listeners: {
                    scope: this,
                    click: this.onAddExaminers
                }
            }, {
                text: 'Random distribute',
                listeners: {
                    scope: this,
                    click: this.onRandomDistributeExaminers
                }
            }, {
                text: 'Clear',
                iconCls: 'icon-delete-16',
                listeners: {
                    scope: this,
                    click: this.onClearExaminers
                }
            }]
        });
        menu.push({
            text: 'Delete',
            iconCls: 'icon-delete-16',
            listeners: {
                scope: this,
                click: function() { Ext.MessageBox.alert('Not available yet', 'Coming soon') }
            }
        });
        return menu;
    }
});
<commit_msg>apps.administrator...StudentsManager: Made change examiners into a separate button.<commit_after>Ext.define('devilry.administrator.studentsmanager.StudentsManager', {
    extend: 'devilry.extjshelpers.studentsmanager.StudentsManager',
    alias: 'widget.administrator_studentsmanager',

    mixins: {
        manageExaminers: 'devilry.administrator.studentsmanager.StudentsManagerManageExaminers',
        createGroups: 'devilry.administrator.studentsmanager.StudentsManagerManageGroups',
        loadRelatedUsers: 'devilry.administrator.studentsmanager.LoadRelatedUsersMixin',
        addDeliveries: 'devilry.administrator.studentsmanager.AddDeliveriesMixin'
    },

    //config: {
        //assignmentgroupPrevApprovedStore: undefined
    //},

    constructor: function(config) {
        this.initConfig(config);
        this.callParent([config]);
    },

    initComponent: function() {
        this.addStudentsButton = Ext.widget('button', {
            text: 'Create groups',
            iconCls: 'icon-add-32',
            scale: 'large',
            menu: [{
                text: Ext.String.format('One group for each student registered in {0}', DevilrySettings.DEVILRY_SYNCSYSTEM),
                listeners: {
                    scope: this,
                    click: this.onOneGroupForEachRelatedStudent
                }
            }, {
                text: 'Manually',
                listeners: {
                    scope: this,
                    click: this.onManuallyCreateUsers
                }
            }]
        });

        this.callParent(arguments);
    },

    getToolbarItems: function() {
        var defaults = this.callParent();
        Ext.Array.insert(defaults, 2, [this.addStudentsButton, {
            xtype: 'button',
            text: 'Set examiners',
            scale: 'large',
            menu: [{
                text: 'Replace',
                iconCls: 'icon-edit-16',
                listeners: {
                    scope: this,
                    click: this.onReplaceExaminers
                }
            }, {
                text: 'Add',
                iconCls: 'icon-add-16',
                listeners: {
                    scope: this,
                    click: this.onAddExaminers
                }
            }, {
                text: 'Random distribute',
                listeners: {
                    scope: this,
                    click: this.onRandomDistributeExaminers
                }
            }, {
                text: 'Clear',
                iconCls: 'icon-delete-16',
                listeners: {
                    scope: this,
                    click: this.onClearExaminers
                }
            }]
        }]);
        return defaults;
    },

    getOnSingleMenuItems: function() {
        var menu = this.callParent();
        //menu.push({
            //text: 'Create dummy delivery',
            //listeners: {
                //scope: this,
                //click: this.onCreateDummyDelivery
            //}
        //});
        menu.push({
            text: 'Change group members',
            iconCls: 'icon-edit-16',
            listeners: {
                scope: this,
                click: this.onChangeGroupMembers
            }
        });
        menu.push({
            text: 'Change group name',
            iconCls: 'icon-edit-16',
            listeners: {
                scope: this,
                click: this.onChangeGroupName
            }
        });
        return menu;
    },

    getOnManyMenuItems: function() {
        var menu = this.callParent();
        menu.push({
            text: 'Mark as delivered in a previous period',
            listeners: {
                scope: this,
                click: this.onPreviouslyPassed
            }
        });
        menu.push({
            text: 'Delete',
            iconCls: 'icon-delete-16',
            listeners: {
                scope: this,
                click: function() { Ext.MessageBox.alert('Not available yet', 'Coming soon') }
            }
        });
        return menu;
    }
});
<|endoftext|>


<commit_before>dojo.provide("org.ppwcode.dojo.dijit.form.PpwDataViewBox");

dojo.require("dijit.layout._LayoutWidget");
dojo.require("dijit._Templated");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.ContentPane");
dojo.require("dojox.grid.Grid");
dojo.require("dijit.form.Button");

dojo.declare("org.ppwcode.dojo.dijit.form.PpwDataViewBox",
	[dijit.layout._LayoutWidget, dijit._Templated],
	{
		templatePath: dojo.moduleUrl("org", "ppwcode/dojo/dijit/form/templates/PpwDataViewBox.html"),

		widgetsInTemplate: true,

		// summary:
		//    HTML attribute to set the DataModel of the Grid encapsulated in the PpwMasterView
		// description:
		//    HTML attribute to set the DataModel of the Grid encapsulated in the PpwMasterView
		gridModel: null,

		// summary:
		//    HTML attribute to set the structure of the Grid encapsulated in the PpwMasterView
		// description:
		//    HTML attribute to set the structure of the Grid encapsulated in the PpwMasterView
		gridStructure: null,

		_selectable: true,

		buildRendering: function() {
			this.inherited(arguments);

			dojo.connect(this._masterGrid, "onRowClick", this, "_ongridrowclick");
			dojo.connect(this._masterGrid, "onHeaderClick", this, "_ongridheaderclick");

			//only allow to select 1 row
			this._masterGrid.selection.multiSelect = false;

			if (this.gridModel) {
				this.setModel(this.gridModel);
			}
			if (this.gridStructure) {
				this.setStructure(this.gridStructure);
			}

		},

  	  	setStructure: function(/*Array*/newstructure) {
			// summary:
			//   Set the structure of the grid that is encapsulated in this PpwMasterView.
			// description:
			//   Set the structure of the grid that is encapsulated in this PpwMasterView.
			//
			// newstructure:
			//   Array containing a Grid structure.
			this._masterGrid.setStructure(newstructure);
		},

		setModel: function(/*Object*/newmodel) {
			//summary:
			//   Set the data model of the grid that is encapsulated in this PpwMasterView
			//description:
			//   Set the data model of the grid that is encapsulated in this PpwMasterView
			// newmodel:
			//   The datamodel that will be used to contain the data displayed in the Grid.
			//delegation to grid
			this._masterGrid.setModel(newmodel);
		},

		setSelectable: function(/*Boolean*/isSelectable) {
			this._selectable = isSelectable;
			this._applySelectable(this._selectable);
		},

		_applySelectable: function(/*Boolean*/isSelectable) {
			this._masterGrid.onCanSelect = function() {
				return isSelectable;
			}
			this._masterGrid.onCanDeselect = function() {
				return isSelectable;
			}
		},

		clearSelection: function() {
			//summary:
			//   Clear the selection in the DataGrid.
			//description:
			//   Clears the selection in the Datagrid.  Calling this method
			//   also triggers the onClearSelection event method.
			if (this._selectable) {
				this._masterGrid.selection.clear();
				this._onclearselection();
			}
		},

		_clearSelection: function() {
			this._applySelectable(true);
			this._masterGrid.selection.clear();
			this._applySelectable(this._selectable);
		},

		setData: function(/*Array*/newdata) {
			//summary:
			//   configure the PpwMasterView with a new dataset that must be displayed in
			//   its Grid.
			//description:
			//   This method is delegated to the Dojo Grid, that is responsible for the actual
			//   displaying of the data.  Calling this method also calls the setData event
			//   method.
			//newdata:
			//   The array containing the new data Items to be displayed in the grid.

			//first clear the selection
			this.clearSelection();
			//delegation to grid's model
			this._masterGrid.model.setData(newdata);
			//call event
			this._onSetData();
		},

        getData: function() {
			//delegation to grid's model
			return this._masterGrid.model.data;
		},

		setValue: function(/*Object*/newValue) {
			this.selectItem(newValue);
		},

		getValue: function() {
			return this.getSelectedItem();
		},

		reset: function() {
			this.clearSelection();
			this.setSelectable(false);
		},

		setAttribute: function(/*String*/ attr, /*anything*/ value) {
			this.inherited(arguments);
			switch(attr){
			case "disabled":
				this.setSelectable(!value);
			}
		},

		getSelectedItem: function() {
			//summary:
			//   Get the selected Item from the grid encapsulated in this PpwMasterView.
			//description:
			//   returns the selected Item from the grid.  Note that multiselect is turned
			//   off by default in PpwMasterView, so there will always be only one selected
			//   Item.  The method hence returns an Object, not an array of objects.
			return this._masterGrid.model.getRow(this._masterGrid.selection.getSelected()[0]);
		},

		selectItem: function(/*Object*/criterium) {
			//summary:
			//   Automatically select an object in the PpwMasterView's Grid based an a selection
			//   criterium.
			//description:
			//   Select a row in the Grid based on a selection criterium.  The selection criterium
			//   is an object that contains at least one property that also exists in the objects
			//   that are being displayed in the grid.  For an Item in the Grid's datamodel to
			//   be selected, the values of all properties in the criterium must be equal to the
			//   values of the corresponding properties in the Item.  Only the first Item that
			//   matches will be selected in the Grid.  If no Item matches the criterium, nothing
			//   will be selected.
			//criterium:
			//   Object with a number of properties (and also values) that must be satisfied for
			//   an Item in the grid to be selected.

			var datasetsize = this._masterGrid.model.getRowCount();
			var found = false;
			var location = -1;
			// copy criterium properties in Array.
			var properties = new Array();
			var tmpi = 0;
			for (properties[tmpi++] in criterium);
			if (properties.length == 0) {
				//no criterium? -> no select
				return;
			}
			//look for the first matching record
			for (var i = 0; !found && (i < datasetsize) ; i++) {
 				for (var j = 0, rowmatch = true; rowmatch && (j < properties.length); j++) {
					if (this._masterGrid.model.getRow(i)[properties[j]] != criterium[properties[j]]) {
						rowmatch = false;
					}
				}
				found = rowmatch;
				location = i;
			}

			if (found) {
				this._applySelectable(true);
				this._masterGrid.scrollToRow(location);
			    //not sure if this is the way to go, but it has the desired result:
			    //the row is selected in the grid, and the detail form shows the
			    //correct information
			    this._masterGrid.doclick({rowIndex: location});
			    this._applySelectable(this.selectable);
			}
		},

		////////////////////////// Layout ////////////////////////////

		layout: function() {
			// stole this from _LayoutWidget:  this calculates the size of the pane
			// in which the grid resides.  Since we're a layoutcontainer, we must tell
			// our children how to draw themselves, so here goes (nothing).
			//var marginbox = dojo.marginBox(this._masterGridPane.domNode);
			//var contentbox = dijit.layout.marginBox2contentBox(this._masterGridPane.domNode, marginbox);

			//if we do not do this call, the Big Bad Grid (tm) doesn't
			//resize() upon resizing of this layout widget
			this.getChildren()[0].resize(this._contentBox);
		},

		////////////////////////// Event handling //////////////////////////

		_onSetData: function() {
			this.onSetData();
		},

		onSetData: function() {
			//summary:
			//   Hook method:  called when the data in the Grid's datamodel is
			//   renewed.
			//description:
			//   Hook method:  called when the data in the Grid's datamodel is
			//   renewed.
		},

		_onclearselection: function() {
			this.onClearSelection();
		},

		onClearSelection: function() {
			//summary:
			//   Hook method: called when the selection in the PpwMasterView's Grid is
			//   cleared.
			//description:
			//   Hook method: called when the selection in the PpwMasterView's Grid is
			//   cleared.
		},

		_ongridrowclick: function(e) {
			//console.log("PpwMasterView: _ongridrowclick");
			//call user event
			this.onGridRowClick(e);
		},

		onGridRowClick: function(event) {
			//summary:
			//   Hook method that is called when a Row in the PpwMasterView is clicked(selected).
			//description:
			//   Hook method that is called when a Row in the PpwMasterView is clicked(selected).
			//event:
			//   The same event that you would receive when receiving a row select event on a
			//   Dojox.Grid.
		},

		_ongridheaderclick: function(e) {
			//console.log("PpwMasterView: _ongridheaderclick");
			//sorting... clear selection.
			this.clearSelection();
			//call user event
			this.onGridHeaderClick();
		},

		onGridHeaderClick: function(e) {
			//summary:
			//   Hook method that is called when a Header row in the PpwMasterView is clicked(selected).
			//description:
			//   Hook method that is called when a Header row in the PpwMasterView is clicked(selected).
			//event:
			//   The same event that you would receive when receiving a header select event on a
			//   Dojox.Grid.
		}
	}
);

dojo.declare("org.ppwcode.dojo.dijit.form.PpwEditableDataViewBox",
	org.ppwcode.dojo.dijit.form.PpwDataViewBox,
	{
		templatePath: dojo.moduleUrl("org", "ppwcode/dojo/dijit/form/templates/PpwEditableDataViewBox.html"),

		widgetsInTemplate: true,

		constructorFunction: null,

		_masterGridCanEditMethod: null,

		buildRendering: function() {
			this.inherited(arguments);
			dojo.connect(this._addButton, "onClick", this, "_onaddbuttonclick");
			dojo.connect(this._deleteButton, "onClick", this, "_ondeletebuttonclick");
			this._masterGridCanEditMethod = this._masterGrid.canEdit;
		},

		setAddButtonDisabled: function(/*boolean*/disable) {
			//summary:
			//   Disable the add button.
			//description:
			//   Disables the add button below the form.
			//disable:
			//   boolean:  pass true if the button must be disabled; false if
			//   the button must be enabled.
			this._addButton.setAttribute('disabled', disable);
		},

		setDeleteButtonDisabled: function(/*boolean*/disable) {
			//summary:
			//   Disable the delete button.
			//description:
			//   Disables the delete button below the form.
			//disable:
			//   boolean:  pass true if the button must be disabled; false if
			//   the button must be enabled.
			this._deleteButton.setAttribute('disabled', disable);
		},

		disableButtons: function(/*boolean*/disable) {
			this.setAddButtonDisabled(disable);
			this.setDeleteButtonDisabled(disable);
		},

        removeSelectedItem: function() {
            this._masterGrid.removeSelectedRows();
        },

        addItem: function(object) {
            this._masterGrid.addRow(object);
        },

        reset: function() {
        	this.inherited(arguments);
        	this._clearSelection();
        	this.setData(null);
        },

        setValue: function(/*Array*/newValue) {
        	this._masterGrid.edit.cancel();
        	this._clearSelection();
        	this.setData(dojo.clone(newValue));
        },

        getValue: function() {
        	// in case we're getting the value while an edit is still in
        	// progress, we first apply that edit and be done with it
        	this._masterGrid.edit.apply();
        	return this.getData();
        },

		setAttribute: function(/*String*/ attr, /*anything*/ value) {
        	this.inherited(arguments);
        	switch(attr) {
        	case "disabled":
        		this.disableButtons(value);
        		//MUDO: we should not assume on the availability of setModifiable here!!!
        		//first we apply any ongoing edit before we turn off modifiability
        		this._masterGrid.edit.apply();
        		this._masterGrid.model.setModifiable(!value);
        	}
        },

        ////////////////////////// Event handling //////////////////////////

		_onaddbuttonclick: function(e) {
			//console.log("PpwMasterView: _onaddbuttonclick");
			this.clearSelection();

			var obj = dojo.isFunction(this.constructorFunction) ? new this.constructorFunction() : new Object();
			this.addItem(obj);
			this.onAddButtonClick(e);
		},

		onAddButtonClick: function(e) {
			//summary:
			//   Hook method for the Add button.
			//description:
			//   Hook method for the Add button.
		},

		_ondeletebuttonclick: function(e) {
			this.removeSelectedItem();
			//console.log("PpwMasterView: _onaddbuttonclick");
			this.onDeleteButtonClick(e);
		},

		onDeleteButtonClick: function(e) {
			//summary:
			//   Hook method for the Delete button.
			//description:
			//   Hook method for the Delete button.
		}
	}
);
<commit_msg>adding support for adding multiple types of objects to DataViewBox<commit_after>dojo.provide("org.ppwcode.dojo.dijit.form.PpwDataViewBox");

dojo.require("dijit.layout._LayoutWidget");
dojo.require("dijit._Templated");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.ContentPane");
dojo.require("dojox.grid.Grid");
dojo.require("dijit.form.Button");

dojo.declare("org.ppwcode.dojo.dijit.form.PpwDataViewBox",
	[dijit.layout._LayoutWidget, dijit._Templated],
	{
		templatePath: dojo.moduleUrl("org", "ppwcode/dojo/dijit/form/templates/PpwDataViewBox.html"),

		widgetsInTemplate: true,

		// summary:
		//    HTML attribute to set the DataModel of the Grid encapsulated in the PpwMasterView
		// description:
		//    HTML attribute to set the DataModel of the Grid encapsulated in the PpwMasterView
		gridModel: null,

		// summary:
		//    HTML attribute to set the structure of the Grid encapsulated in the PpwMasterView
		// description:
		//    HTML attribute to set the structure of the Grid encapsulated in the PpwMasterView
		gridStructure: null,

		_selectable: true,

		buildRendering: function() {
			this.inherited(arguments);

			dojo.connect(this._masterGrid, "onRowClick", this, "_ongridrowclick");
			dojo.connect(this._masterGrid, "onHeaderClick", this, "_ongridheaderclick");

			//only allow to select 1 row
			this._masterGrid.selection.multiSelect = false;

			if (this.gridModel) {
				this.setModel(this.gridModel);
			}
			if (this.gridStructure) {
				this.setStructure(this.gridStructure);
			}

		},

  	  	setStructure: function(/*Array*/newstructure) {
			// summary:
			//   Set the structure of the grid that is encapsulated in this PpwMasterView.
			// description:
			//   Set the structure of the grid that is encapsulated in this PpwMasterView.
			//
			// newstructure:
			//   Array containing a Grid structure.
			this._masterGrid.setStructure(newstructure);
		},

		setModel: function(/*Object*/newmodel) {
			//summary:
			//   Set the data model of the grid that is encapsulated in this PpwMasterView
			//description:
			//   Set the data model of the grid that is encapsulated in this PpwMasterView
			// newmodel:
			//   The datamodel that will be used to contain the data displayed in the Grid.
			//delegation to grid
			this._masterGrid.setModel(newmodel);
		},

		setSelectable: function(/*Boolean*/isSelectable) {
			this._selectable = isSelectable;
			this._applySelectable(this._selectable);
		},

		_applySelectable: function(/*Boolean*/isSelectable) {
			this._masterGrid.onCanSelect = function() {
				return isSelectable;
			}
			this._masterGrid.onCanDeselect = function() {
				return isSelectable;
			}
		},

		clearSelection: function() {
			//summary:
			//   Clear the selection in the DataGrid.
			//description:
			//   Clears the selection in the Datagrid.  Calling this method
			//   also triggers the onClearSelection event method.
			if (this._selectable) {
				this._masterGrid.selection.clear();
				this._onclearselection();
			}
		},

		_clearSelection: function() {
			this._applySelectable(true);
			this._masterGrid.selection.clear();
			this._applySelectable(this._selectable);
		},

		setData: function(/*Array*/newdata) {
			//summary:
			//   configure the PpwMasterView with a new dataset that must be displayed in
			//   its Grid.
			//description:
			//   This method is delegated to the Dojo Grid, that is responsible for the actual
			//   displaying of the data.  Calling this method also calls the setData event
			//   method.
			//newdata:
			//   The array containing the new data Items to be displayed in the grid.

			//first clear the selection
			this.clearSelection();
			//delegation to grid's model
			this._masterGrid.model.setData(newdata);
			//call event
			this._onSetData();
		},

        getData: function() {
			//delegation to grid's model
			return this._masterGrid.model.data;
		},

		setValue: function(/*Object*/newValue) {
			this.selectItem(newValue);
		},

		getValue: function() {
			return this.getSelectedItem();
		},

		reset: function() {
			this.clearSelection();
			this.setSelectable(false);
		},

		setAttribute: function(/*String*/ attr, /*anything*/ value) {
			this.inherited(arguments);
			switch(attr){
			case "disabled":
				this.setSelectable(!value);
			}
		},

		getSelectedItem: function() {
			//summary:
			//   Get the selected Item from the grid encapsulated in this PpwMasterView.
			//description:
			//   returns the selected Item from the grid.  Note that multiselect is turned
			//   off by default in PpwMasterView, so there will always be only one selected
			//   Item.  The method hence returns an Object, not an array of objects.
			return this._masterGrid.model.getRow(this._masterGrid.selection.getSelected()[0]);
		},

		selectItem: function(/*Object*/criterium) {
			//summary:
			//   Automatically select an object in the PpwMasterView's Grid based an a selection
			//   criterium.
			//description:
			//   Select a row in the Grid based on a selection criterium.  The selection criterium
			//   is an object that contains at least one property that also exists in the objects
			//   that are being displayed in the grid.  For an Item in the Grid's datamodel to
			//   be selected, the values of all properties in the criterium must be equal to the
			//   values of the corresponding properties in the Item.  Only the first Item that
			//   matches will be selected in the Grid.  If no Item matches the criterium, nothing
			//   will be selected.
			//criterium:
			//   Object with a number of properties (and also values) that must be satisfied for
			//   an Item in the grid to be selected.

			var datasetsize = this._masterGrid.model.getRowCount();
			var found = false;
			var location = -1;
			// copy criterium properties in Array.
			var properties = new Array();
			var tmpi = 0;
			for (properties[tmpi++] in criterium);
			if (properties.length == 0) {
				//no criterium? -> no select
				return;
			}
			//look for the first matching record
			for (var i = 0; !found && (i < datasetsize) ; i++) {
 				for (var j = 0, rowmatch = true; rowmatch && (j < properties.length); j++) {
					if (this._masterGrid.model.getRow(i)[properties[j]] != criterium[properties[j]]) {
						rowmatch = false;
					}
				}
				found = rowmatch;
				location = i;
			}

			if (found) {
				this._applySelectable(true);
				this._masterGrid.scrollToRow(location);
			    //not sure if this is the way to go, but it has the desired result:
			    //the row is selected in the grid, and the detail form shows the
			    //correct information
			    this._masterGrid.doclick({rowIndex: location});
			    this._applySelectable(this.selectable);
			}
		},

		////////////////////////// Layout ////////////////////////////

		layout: function() {
			// stole this from _LayoutWidget:  this calculates the size of the pane
			// in which the grid resides.  Since we're a layoutcontainer, we must tell
			// our children how to draw themselves, so here goes (nothing).
			//var marginbox = dojo.marginBox(this._masterGridPane.domNode);
			//var contentbox = dijit.layout.marginBox2contentBox(this._masterGridPane.domNode, marginbox);

			//if we do not do this call, the Big Bad Grid (tm) doesn't
			//resize() upon resizing of this layout widget
			this.getChildren()[0].resize(this._contentBox);
		},

		////////////////////////// Event handling //////////////////////////

		_onSetData: function() {
			this.onSetData();
		},

		onSetData: function() {
			//summary:
			//   Hook method:  called when the data in the Grid's datamodel is
			//   renewed.
			//description:
			//   Hook method:  called when the data in the Grid's datamodel is
			//   renewed.
		},

		_onclearselection: function() {
			this.onClearSelection();
		},

		onClearSelection: function() {
			//summary:
			//   Hook method: called when the selection in the PpwMasterView's Grid is
			//   cleared.
			//description:
			//   Hook method: called when the selection in the PpwMasterView's Grid is
			//   cleared.
		},

		_ongridrowclick: function(e) {
			//console.log("PpwMasterView: _ongridrowclick");
			//call user event
			this.onGridRowClick(e);
		},

		onGridRowClick: function(event) {
			//summary:
			//   Hook method that is called when a Row in the PpwMasterView is clicked(selected).
			//description:
			//   Hook method that is called when a Row in the PpwMasterView is clicked(selected).
			//event:
			//   The same event that you would receive when receiving a row select event on a
			//   Dojox.Grid.
		},

		_ongridheaderclick: function(e) {
			//console.log("PpwMasterView: _ongridheaderclick");
			//sorting... clear selection.
			this.clearSelection();
			//call user event
			this.onGridHeaderClick();
		},

		onGridHeaderClick: function(e) {
			//summary:
			//   Hook method that is called when a Header row in the PpwMasterView is clicked(selected).
			//description:
			//   Hook method that is called when a Header row in the PpwMasterView is clicked(selected).
			//event:
			//   The same event that you would receive when receiving a header select event on a
			//   Dojox.Grid.
		}
	}
);

dojo.declare("org.ppwcode.dojo.dijit.form.PpwEditableDataViewBox",
	org.ppwcode.dojo.dijit.form.PpwDataViewBox,
	{
		templatePath: dojo.moduleUrl("org", "ppwcode/dojo/dijit/form/templates/PpwEditableDataViewBox.html"),

		widgetsInTemplate: true,

		constructorFunction: null,

		_addChooser: null,
		_constructorNameToConstructorMap: null,

		buildRendering: function() {
			this.inherited(arguments);
			dojo.connect(this._addButton, "onClick", this, "_onaddbuttonclick");
			dojo.connect(this._deleteButton, "onClick", this, "_ondeletebuttonclick");
		},

		setAddButtonDisabled: function(/*boolean*/disable) {
			//summary:
			//   Disable the add button.
			//description:
			//   Disables the add button below the form.
			//disable:
			//   boolean:  pass true if the button must be disabled; false if
			//   the button must be enabled.
			this._addButton.setAttribute('disabled', disable);
			if (this._addChooser) {
				this._addChooser.setAttribute('disabled', disable);
			}
		},

		setDeleteButtonDisabled: function(/*boolean*/disable) {
			//summary:
			//   Disable the delete button.
			//description:
			//   Disables the delete button below the form.
			//disable:
			//   boolean:  pass true if the button must be disabled; false if
			//   the button must be enabled.
			this._deleteButton.setAttribute('disabled', disable);
		},

		disableButtons: function(/*boolean*/disable) {
			this.setAddButtonDisabled(disable);
			this.setDeleteButtonDisabled(disable);
		},

		setMultiButton: function(/*Array*/nameConstructorMap) {
			//remove the current widget;
			this._addChooser = null;

			this._constructorNameToConstructorMap = new Object();

			var buttonData = {
					identifier: "value",
			        label: "label",
			        items: []
			};

			for (var i = 0; i < nameConstructorMap.length; i++) {
				var ctorname = org.ppwcode.dojo.util.JavaScriptHelpers.getFunctionName(nameConstructorMap[i].constructorFunction)
				buttonData.items[i] = new Object();
				buttonData.items[i].value = ctorname;
				buttonData.items[i].label = nameConstructorMap[i].objectName;
				this._constructorNameToConstructorMap[ctorname] = nameConstructorMap[i].constructorFunction;
			}
			var buttonDataStore = new dojo.data.ItemFileReadStore({data: buttonData});
			var inputNode = dojo.doc.createElement('button');
			this._buttonPane.domNode.insertBefore(inputNode, this._addButton.domNode);
			this._addChooser =
				new org.ppwcode.dojo.dojox.DataDropDown({store: buttonDataStore}, inputNode);
			this._addChooser.startup();
		},

        removeSelectedItem: function() {
            this._masterGrid.removeSelectedRows();
        },

        addItem: function(object) {
            this._masterGrid.addRow(object);
        },

        reset: function() {
        	this.inherited(arguments);
        	this._clearSelection();
        	this.setData(null);
        },

        setValue: function(/*Array*/newValue) {
        	this._masterGrid.edit.cancel();
        	this._clearSelection();
        	this.setData(dojo.clone(newValue));
        },

        getValue: function() {
        	// in case we're getting the value while an edit is still in
        	// progress, we first apply that edit and be done with it
        	this._masterGrid.edit.apply();
        	return this.getData();
        },

		setAttribute: function(/*String*/ attr, /*anything*/ value) {
        	this.inherited(arguments);
        	switch(attr) {
        	case "disabled":
        		this.disableButtons(value);
        		//MUDO: we should not assume on the availability of setModifiable here!!!
        		//first we apply any ongoing edit before we turn off modifiability
        		this._masterGrid.edit.apply();
        		this._masterGrid.model.setModifiable(!value);
        		break;
        	}
        },

        ////////////////////////// Event handling //////////////////////////

		_onaddbuttonclick: function(e) {
			//console.log("PpwMasterView: _onaddbuttonclick");
			this.clearSelection();
			var obj = null;
			if (this._addChooser != null) {
				var val = this._addChooser.value;
				var ctor = this._constructorNameToConstructorMap[val];
				obj = new ctor();
			} else {
				obj = dojo.isFunction(this.constructorFunction) ? new this.constructorFunction() : new Object();
			}
			this.addItem(obj);
			this.onAddButtonClick(e);
		},

		onAddButtonClick: function(e) {
			//summary:
			//   Hook method for the Add button.
			//description:
			//   Hook method for the Add button.
		},

		_ondeletebuttonclick: function(e) {
			this.removeSelectedItem();
			//console.log("PpwMasterView: _onaddbuttonclick");
			this.onDeleteButtonClick(e);
		},

		onDeleteButtonClick: function(e) {
			//summary:
			//   Hook method for the Delete button.
			//description:
			//   Hook method for the Delete button.
		}
	}
);
<|endoftext|>


<commit_before>jest.unmock('../parseArgs');

import parseArgs from '../parseArgs';

describe('parseArgs()', () => {
  it('correctly parses args', () => {
    const command = 'ls -l --goose-mode /home/lawrence';
    const args = parseArgs(command);

    expect(args).toEqual({
      options: ['goose-mode'],
      flags: ['l'],
      targets: ['/home/lawrence'],
    });
  });

  it('parses multiple flags', () => {
    const command = 'ls -al lmao';
    const args = parseArgs(command);

    expect(args).toEqual({
      options: [],
      flags: ['a', 'l'],
      targets: ['lmao'],
    });
  });

  it('sorts multiple flags', () => {
    const command = 'ls --bsian --csian --asian paninos';
    const args = parseArgs(command);

    expect(args).toEqual({
      options: ['asian', 'bsian', 'csian'],
      flags: [],
      targets: ['paninos'],
    });
  });
});
<commit_msg>parse args api changed<commit_after>jest.unmock('../parseArgs');

import parseArgs from '../parseArgs';

describe('parseArgs()', () => {
  it('correctly parses args', () => {
    const command = 'ls -l --goose-mode /home/lawrence';
    const args = parseArgs(command);

    expect(args).toEqual({
      fulls: ['goose-mode'],
      flags: ['l'],
      targets: ['/home/lawrence'],
    });
  });

  it('parses multiple flags', () => {
    const command = 'ls -al lmao';
    const args = parseArgs(command);

    expect(args).toEqual({
      fulls: [],
      flags: ['a', 'l'],
      targets: ['lmao'],
    });
  });

  it('sorts multiple flags', () => {
    const command = 'ls --bsian --csian --asian paninos';
    const args = parseArgs(command);

    expect(args).toEqual({
      fulls: ['asian', 'bsian', 'csian'],
      flags: [],
      targets: ['paninos'],
    });
  });
});
<|endoftext|>


<commit_before>import qs from 'qs';
import { isEmpty, isNil, pipe, reject } from 'ramda';
import PropTypes from 'prop-types';

export const apiErrorType = PropTypes.shape({
  type: PropTypes.string,
  detail: PropTypes.string,
  title: PropTypes.string,
  status: PropTypes.number,
  error: PropTypes.string, // Deprecated
  message: PropTypes.string, // Deprecated
});

const buildShlinkBaseUrl = (url, apiVersion) => url ? `${url}/rest/v${apiVersion}` : '';
const rejectNilProps = (options = {}) => reject(isNil, options);

export default class ShlinkApiClient {
  constructor(axios, baseUrl, apiKey) {
    this.axios = axios;
    this._apiVersion = 2;
    this._baseUrl = baseUrl;
    this._apiKey = apiKey || '';
  }

  listShortUrls = pipe(
    rejectNilProps,
    (options) => this._performRequest('/short-urls', 'GET', options).then((resp) => resp.data.shortUrls)
  );

  createShortUrl = (options) => {
    const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options);

    return this._performRequest('/short-urls', 'POST', {}, filteredOptions)
      .then((resp) => resp.data);
  };

  getShortUrlVisits = (shortCode, query) =>
    this._performRequest(`/short-urls/${shortCode}/visits`, 'GET', query)
      .then((resp) => resp.data.visits);

  getShortUrl = (shortCode, domain) =>
    this._performRequest(`/short-urls/${shortCode}`, 'GET', { domain })
      .then((resp) => resp.data);

  deleteShortUrl = (shortCode, domain) =>
    this._performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
      .then(() => ({}));

  updateShortUrlTags = (shortCode, domain, tags) =>
    this._performRequest(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags })
      .then((resp) => resp.data.tags);

  updateShortUrlMeta = (shortCode, domain, meta) =>
    this._performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta)
      .then(() => meta);

  listTags = () =>
    this._performRequest('/tags', 'GET')
      .then((resp) => resp.data.tags.data);

  deleteTags = (tags) =>
    this._performRequest('/tags', 'DELETE', { tags })
      .then(() => ({ tags }));

  editTag = (oldName, newName) =>
    this._performRequest('/tags', 'PUT', {}, { oldName, newName })
      .then(() => ({ oldName, newName }));

  health = () => this._performRequest('/health', 'GET').then((resp) => resp.data);

  _performRequest = async (url, method = 'GET', query = {}, body = {}) => {
    try {
      return await this.axios({
        method,
        url: `${buildShlinkBaseUrl(this._baseUrl, this._apiVersion)}${url}`,
        headers: { 'X-Api-Key': this._apiKey },
        params: rejectNilProps(query),
        data: body,
        paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
      });
    } catch (e) {
      const { response } = e;

      // Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error
      // when performed from the browser (due to the preflight request not returning a 2xx status.
      // See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here.
      // The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as
      // if a request has been performed to a not supported API version.
      const apiVersionIsNotSupported = !response;

      // When the request is not invalid or we have already tried both API versions, throw the error and let the
      // caller handle it
      if (!apiVersionIsNotSupported || this._apiVersion === 1) {
        throw e;
      }

      this._apiVersion = 1;

      return await this._performRequest(url, method, query, body);
    }
  }
}
<commit_msg>Removed redundant function call<commit_after>import qs from 'qs';
import { isEmpty, isNil, reject } from 'ramda';
import PropTypes from 'prop-types';

export const apiErrorType = PropTypes.shape({
  type: PropTypes.string,
  detail: PropTypes.string,
  title: PropTypes.string,
  status: PropTypes.number,
  error: PropTypes.string, // Deprecated
  message: PropTypes.string, // Deprecated
});

const buildShlinkBaseUrl = (url, apiVersion) => url ? `${url}/rest/v${apiVersion}` : '';
const rejectNilProps = reject(isNil);

export default class ShlinkApiClient {
  constructor(axios, baseUrl, apiKey) {
    this.axios = axios;
    this._apiVersion = 2;
    this._baseUrl = baseUrl;
    this._apiKey = apiKey || '';
  }

  listShortUrls = (options = {}) =>
    this._performRequest('/short-urls', 'GET', options).then((resp) => resp.data.shortUrls);

  createShortUrl = (options) => {
    const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options);

    return this._performRequest('/short-urls', 'POST', {}, filteredOptions)
      .then((resp) => resp.data);
  };

  getShortUrlVisits = (shortCode, query) =>
    this._performRequest(`/short-urls/${shortCode}/visits`, 'GET', query)
      .then((resp) => resp.data.visits);

  getShortUrl = (shortCode, domain) =>
    this._performRequest(`/short-urls/${shortCode}`, 'GET', { domain })
      .then((resp) => resp.data);

  deleteShortUrl = (shortCode, domain) =>
    this._performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
      .then(() => ({}));

  updateShortUrlTags = (shortCode, domain, tags) =>
    this._performRequest(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags })
      .then((resp) => resp.data.tags);

  updateShortUrlMeta = (shortCode, domain, meta) =>
    this._performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta)
      .then(() => meta);

  listTags = () =>
    this._performRequest('/tags', 'GET')
      .then((resp) => resp.data.tags.data);

  deleteTags = (tags) =>
    this._performRequest('/tags', 'DELETE', { tags })
      .then(() => ({ tags }));

  editTag = (oldName, newName) =>
    this._performRequest('/tags', 'PUT', {}, { oldName, newName })
      .then(() => ({ oldName, newName }));

  health = () => this._performRequest('/health', 'GET').then((resp) => resp.data);

  _performRequest = async (url, method = 'GET', query = {}, body = {}) => {
    try {
      return await this.axios({
        method,
        url: `${buildShlinkBaseUrl(this._baseUrl, this._apiVersion)}${url}`,
        headers: { 'X-Api-Key': this._apiKey },
        params: rejectNilProps(query),
        data: body,
        paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
      });
    } catch (e) {
      const { response } = e;

      // Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error
      // when performed from the browser (due to the preflight request not returning a 2xx status.
      // See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here.
      // The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as
      // if a request has been performed to a not supported API version.
      const apiVersionIsNotSupported = !response;

      // When the request is not invalid or we have already tried both API versions, throw the error and let the
      // caller handle it
      if (!apiVersionIsNotSupported || this._apiVersion === 1) {
        throw e;
      }

      this._apiVersion = 1;

      return await this._performRequest(url, method, query, body);
    }
  }
}
<|endoftext|>


<commit_before>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/internal-plugin-board';

import {assert} from '@ciscospark/test-helper-chai';
import {maxWaitForEvent} from '@ciscospark/test-helper-mocha';
import CiscoSpark from '@ciscospark/spark-core';
import testUsers from '@ciscospark/test-helper-test-users';
import uuid from 'uuid';
import promiseSeries from 'es6-promise-series';

describe('plugin-board', () => {
  describe('realtime - sharing mercury', () => {
    let board, conversation, participants, secondBoard;
    let mccoy, spock;
    let uniqueRealtimeData;

    before('create users', () => testUsers.create({count: 2})
      .then((users) => {
        participants = [spock, mccoy] = users;

        return Promise.all(participants.map((participant) => {
          participant.spark = new CiscoSpark({
            credentials: {
              authorization: participant.token
            }
          });
          return participant.spark.internal.device.register()
            .then(() => participant.spark.internal.feature.setFeature('developer', 'web-shared-mercury', true));
        }));
      }));

    before('create conversation', () => spock.spark.internal.conversation.create({
      displayName: 'Test Board Mercury',
      participants
    })
      .then((c) => {
        conversation = c;
        return conversation;
      }));

    before('create channel (board)', () => spock.spark.internal.board.createChannel(conversation)
      .then((channel) => {
        board = channel;
        return channel;
      }));

    before('create second channel (board)', () => mccoy.spark.internal.board.createChannel(conversation)
      .then((channel) => {
        secondBoard = channel;
        return channel;
      }));

    beforeEach('connect to mercury channel', () => Promise.all(participants.map((participant) => participant.spark.internal.mercury.connect())));

    afterEach('disconnect mercury', () => Promise.all(participants.map((participant) => participant.spark.internal.mercury.disconnect())));

    describe('#publish()', () => {
      describe('string payload', () => {
        let spockRealtimeChannel;
        let mccoyRealtimeChannel;

        before('open two connections', () => Promise.all([
          spock.spark.internal.board.realtime.connectToSharedMercury(board),
          mccoy.spark.internal.board.realtime.connectByOpenNewMercuryConnection(board)
        ]));

        before('get realtime channels', () => {
          spockRealtimeChannel = spock.spark.internal.board.realtime.realtimeChannels.get(board.channelId);
          mccoyRealtimeChannel = mccoy.spark.internal.board.realtime.realtimeChannels.get(board.channelId);
        });

        after(() => Promise.all(participants.map((participant) => participant.spark.internal.board.realtime.disconnectFromSharedMercury(board))));

        it('posts a message from shared connection to the specified board', () => {
          uniqueRealtimeData = uuid.v4();
          const data = {
            envelope: {
              channelId: board,
              roomId: conversation.id
            },
            payload: {
              msg: uniqueRealtimeData
            }
          };

          // confirm that both are connected.
          assert.isTrue(spockRealtimeChannel.isSharingMercury, 'spock is sharing mercury connection');
          assert.isTrue(spock.spark.internal.mercury.connected, 'spock is connected');
          assert.isFalse(mccoyRealtimeChannel.isSharingMercury, 'mccoy should not share mercury connection');
          assert.isTrue(mccoy.spark.internal.mercury.connected, 'mccoy is connected');

          spock.spark.internal.board.realtime.publish(board, data);
          // mccoy listens and verifies data received
          return maxWaitForEvent(5000, 'event:board.activity', mccoyRealtimeChannel)
            .then(({data}) => {
              assert.equal(data.contentType, 'STRING');
              assert.equal(data.payload.msg, uniqueRealtimeData);
            });
        });

        it('posts a message from separated socket connection to the specified board', () => {
          uniqueRealtimeData = uuid.v4();
          const data = {
            envelope: {
              channelId: board,
              roomId: conversation.id
            },
            payload: {
              msg: uniqueRealtimeData
            }
          };

          assert.isTrue(spock.spark.internal.mercury.connected, 'spock is connected');
          assert.isTrue(spockRealtimeChannel.isSharingMercury, 'spock is sharing mercury connection');
          assert.isTrue(mccoy.spark.internal.mercury.connected, 'mccoy is connected');
          assert.isFalse(mccoyRealtimeChannel.isSharingMercury, 'mccoy does not share mercury connection');

          mccoy.spark.internal.board.realtime.publish(board, data);
          return maxWaitForEvent(5000, 'event:board.activity', spockRealtimeChannel)
            .then(({data}) => {
              assert.equal(data.contentType, 'STRING');
              assert.equal(data.payload.msg, uniqueRealtimeData);
            });
        });
      });
    });

    describe('multiple boards sharing a connection', () => {
      beforeEach(() => {
        uniqueRealtimeData = uuid.v4();
        return promiseSeries([
          spock.spark.internal.board.realtime.connectToSharedMercury.bind(spock.spark.internal.board.realtime, board),
          mccoy.spark.internal.board.realtime.connectToSharedMercury.bind(mccoy.spark.internal.board.realtime, board),
          spock.spark.internal.board.realtime.connectToSharedMercury.bind(spock.spark.internal.board.realtime, secondBoard),
          mccoy.spark.internal.board.realtime.connectToSharedMercury.bind(mccoy.spark.internal.board.realtime, secondBoard)
        ]);
      });

      afterEach(() => Promise.all(participants.map((participant) => promiseSeries([
        participant.spark.internal.board.realtime.disconnectFromSharedMercury.bind(participant.spark.internal.board.realtime, board),
        participant.spark.internal.board.realtime.disconnectFromSharedMercury.bind(participant.spark.internal.board.realtime, secondBoard)
      ]))));

      it('receives correct message for the corresponding board', (done) => {
        const data1 = {
          envelope: {
            channelId: board,
            roomId: conversation.id
          },
          payload: {
            msg: `first message for ${board.channelId} ${uniqueRealtimeData}`
          }
        };
        const data2 = {
          envelope: {
            channelId: secondBoard,
            roomId: conversation.id
          },
          payload: {
            msg: `second message for ${secondBoard.channelId} ${uniqueRealtimeData}`
          }
        };

        let receivedFirstMsg = false;
        let receivedSecondMsg = false;
        const mccoyRealtimeChannel0 = mccoy.spark.internal.board.realtime.realtimeChannels.get(board.channelId);
        const mccoyRealtimeChannel1 = mccoy.spark.internal.board.realtime.realtimeChannels.get(secondBoard.channelId);

        mccoyRealtimeChannel0.once('event:board.activity', ({data}) => {
          assert.equal(data.contentType, 'STRING');
          assert.equal(data.payload.msg, `first message for ${board.channelId} ${uniqueRealtimeData}`);
          receivedFirstMsg = true;
          if (receivedFirstMsg && receivedSecondMsg) {
            done();
          }
        });

        mccoyRealtimeChannel1.once('event:board.activity', ({data}) => {
          assert.equal(data.contentType, 'STRING');
          assert.equal(data.payload.msg, `second message for ${secondBoard.channelId} ${uniqueRealtimeData}`);
          receivedSecondMsg = true;
          if (receivedFirstMsg && receivedSecondMsg) {
            done();
          }
        });

        Promise.all([
          spock.spark.internal.board.realtime.publish(board, data1),
          spock.spark.internal.board.realtime.publish(secondBoard, data2)
        ]);
      });
    });
  });
});
<commit_msg>test(internal-plugin-board): skip test until RCA can be performed<commit_after>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/internal-plugin-board';

import {assert} from '@ciscospark/test-helper-chai';
import {maxWaitForEvent} from '@ciscospark/test-helper-mocha';
import CiscoSpark from '@ciscospark/spark-core';
import testUsers from '@ciscospark/test-helper-test-users';
import uuid from 'uuid';
import promiseSeries from 'es6-promise-series';

describe('plugin-board', () => {
  describe.skip('realtime - sharing mercury', () => {
    let board, conversation, participants, secondBoard;
    let mccoy, spock;
    let uniqueRealtimeData;

    before('create users', () => testUsers.create({count: 2})
      .then((users) => {
        participants = [spock, mccoy] = users;

        return Promise.all(participants.map((participant) => {
          participant.spark = new CiscoSpark({
            credentials: {
              authorization: participant.token
            }
          });
          return participant.spark.internal.device.register()
            .then(() => participant.spark.internal.feature.setFeature('developer', 'web-shared-mercury', true));
        }));
      }));

    before('create conversation', () => spock.spark.internal.conversation.create({
      displayName: 'Test Board Mercury',
      participants
    })
      .then((c) => {
        conversation = c;
        return conversation;
      }));

    before('create channel (board)', () => spock.spark.internal.board.createChannel(conversation)
      .then((channel) => {
        board = channel;
        return channel;
      }));

    before('create second channel (board)', () => mccoy.spark.internal.board.createChannel(conversation)
      .then((channel) => {
        secondBoard = channel;
        return channel;
      }));

    beforeEach('connect to mercury channel', () => Promise.all(participants.map((participant) => participant.spark.internal.mercury.connect())));

    afterEach('disconnect mercury', () => Promise.all(participants.map((participant) => participant.spark.internal.mercury.disconnect())));

    describe('#publish()', () => {
      describe('string payload', () => {
        let spockRealtimeChannel;
        let mccoyRealtimeChannel;

        before('open two connections', () => Promise.all([
          spock.spark.internal.board.realtime.connectToSharedMercury(board),
          mccoy.spark.internal.board.realtime.connectByOpenNewMercuryConnection(board)
        ]));

        before('get realtime channels', () => {
          spockRealtimeChannel = spock.spark.internal.board.realtime.realtimeChannels.get(board.channelId);
          mccoyRealtimeChannel = mccoy.spark.internal.board.realtime.realtimeChannels.get(board.channelId);
        });

        after(() => Promise.all(participants.map((participant) => participant.spark.internal.board.realtime.disconnectFromSharedMercury(board))));

        it('posts a message from shared connection to the specified board', () => {
          uniqueRealtimeData = uuid.v4();
          const data = {
            envelope: {
              channelId: board,
              roomId: conversation.id
            },
            payload: {
              msg: uniqueRealtimeData
            }
          };

          // confirm that both are connected.
          assert.isTrue(spockRealtimeChannel.isSharingMercury, 'spock is sharing mercury connection');
          assert.isTrue(spock.spark.internal.mercury.connected, 'spock is connected');
          assert.isFalse(mccoyRealtimeChannel.isSharingMercury, 'mccoy should not share mercury connection');
          assert.isTrue(mccoy.spark.internal.mercury.connected, 'mccoy is connected');

          spock.spark.internal.board.realtime.publish(board, data);
          // mccoy listens and verifies data received
          return maxWaitForEvent(5000, 'event:board.activity', mccoyRealtimeChannel)
            .then(({data}) => {
              assert.equal(data.contentType, 'STRING');
              assert.equal(data.payload.msg, uniqueRealtimeData);
            });
        });

        it('posts a message from separated socket connection to the specified board', () => {
          uniqueRealtimeData = uuid.v4();
          const data = {
            envelope: {
              channelId: board,
              roomId: conversation.id
            },
            payload: {
              msg: uniqueRealtimeData
            }
          };

          assert.isTrue(spock.spark.internal.mercury.connected, 'spock is connected');
          assert.isTrue(spockRealtimeChannel.isSharingMercury, 'spock is sharing mercury connection');
          assert.isTrue(mccoy.spark.internal.mercury.connected, 'mccoy is connected');
          assert.isFalse(mccoyRealtimeChannel.isSharingMercury, 'mccoy does not share mercury connection');

          mccoy.spark.internal.board.realtime.publish(board, data);
          return maxWaitForEvent(5000, 'event:board.activity', spockRealtimeChannel)
            .then(({data}) => {
              assert.equal(data.contentType, 'STRING');
              assert.equal(data.payload.msg, uniqueRealtimeData);
            });
        });
      });
    });

    describe('multiple boards sharing a connection', () => {
      beforeEach(() => {
        uniqueRealtimeData = uuid.v4();
        return promiseSeries([
          spock.spark.internal.board.realtime.connectToSharedMercury.bind(spock.spark.internal.board.realtime, board),
          mccoy.spark.internal.board.realtime.connectToSharedMercury.bind(mccoy.spark.internal.board.realtime, board),
          spock.spark.internal.board.realtime.connectToSharedMercury.bind(spock.spark.internal.board.realtime, secondBoard),
          mccoy.spark.internal.board.realtime.connectToSharedMercury.bind(mccoy.spark.internal.board.realtime, secondBoard)
        ]);
      });

      afterEach(() => Promise.all(participants.map((participant) => promiseSeries([
        participant.spark.internal.board.realtime.disconnectFromSharedMercury.bind(participant.spark.internal.board.realtime, board),
        participant.spark.internal.board.realtime.disconnectFromSharedMercury.bind(participant.spark.internal.board.realtime, secondBoard)
      ]))));

      it('receives correct message for the corresponding board', (done) => {
        const data1 = {
          envelope: {
            channelId: board,
            roomId: conversation.id
          },
          payload: {
            msg: `first message for ${board.channelId} ${uniqueRealtimeData}`
          }
        };
        const data2 = {
          envelope: {
            channelId: secondBoard,
            roomId: conversation.id
          },
          payload: {
            msg: `second message for ${secondBoard.channelId} ${uniqueRealtimeData}`
          }
        };

        let receivedFirstMsg = false;
        let receivedSecondMsg = false;
        const mccoyRealtimeChannel0 = mccoy.spark.internal.board.realtime.realtimeChannels.get(board.channelId);
        const mccoyRealtimeChannel1 = mccoy.spark.internal.board.realtime.realtimeChannels.get(secondBoard.channelId);

        mccoyRealtimeChannel0.once('event:board.activity', ({data}) => {
          assert.equal(data.contentType, 'STRING');
          assert.equal(data.payload.msg, `first message for ${board.channelId} ${uniqueRealtimeData}`);
          receivedFirstMsg = true;
          if (receivedFirstMsg && receivedSecondMsg) {
            done();
          }
        });

        mccoyRealtimeChannel1.once('event:board.activity', ({data}) => {
          assert.equal(data.contentType, 'STRING');
          assert.equal(data.payload.msg, `second message for ${secondBoard.channelId} ${uniqueRealtimeData}`);
          receivedSecondMsg = true;
          if (receivedFirstMsg && receivedSecondMsg) {
            done();
          }
        });

        Promise.all([
          spock.spark.internal.board.realtime.publish(board, data1),
          spock.spark.internal.board.realtime.publish(secondBoard, data2)
        ]);
      });
    });
  });
});
<|endoftext|>


<commit_before>"use strict";

var cp = require('child_process');
var fs = require('fs');

var Commits = require('./commits');
var Cache = require('./cache');
var utils = require('./utils');

var spawn = cp.spawn;
var basename = require('path').basename;
var join = require('path').join;
var debug = require('debug')('pushhub');

function sortByName(a, b) {
    if(a.name < b.name) return -1;
    if(a.name === b.name) return 0;
    if(a.name > b.name) return 1;
}

/**
 * Initialize a new `Repo` with the given `path`
 *
 * @param {String} path
 * @api public
 */

var Repo = module.exports = function Repo(path) {
    this.name = basename(path);
    this.path = path;
    this.cache = new Cache();
    this.bare = !utils.isDirectory(join(path, '.git'));
};

/**
 * Parse text coming from `git ls-tree` into an `Array` of `Object`s
 *
 * @param {String} text
 * @api private
 * @return {Array}
 */

Repo.prototype.parse = function parse(text) {
    text = text.toString();
    return text.split('\n').map(function(line) {
        var o = {
            'mode': line.slice(0, 6),
            'type': line.slice(7, 11),
            'hash': line.slice(12, 52),
            'path': line.slice(53)
        };
        // Useful to distinguish folders from revision/refs
        o.path += (o.type == 'tree') ? '/' : '';
        o.name = basename(o.path);
        return o;
    }, this);
};

/**
 * Proxy method to git commands, we don't use cp.exec here because the
 * stdout it returns is a `String` as it cause problems with binary
 * files
 *
 * @param {Array} commands
 * @param {Function} cb
 * @api private
 */

Repo.prototype.gitExec = function gitExec(commands, cb) {
    var child = spawn("git", commands, {cwd: this.path});
    var stdout = [], stderr = [];

    debug('[SPAWN]: %s', "git " + commands.join(" "));
    child.stdout.on('data', function (text) {
        stdout[stdout.length] = text;
    });
    child.stderr.on('data', function (text) {
        stderr[stderr.length] = text;
    });
    child.on('close', function (code) {
        if (code > 0) {
            var err = new Error("git " + commands.join(" ") + "\n" + utils.bufferConcat(stderr, 'utf8'));
            cb(err);
            return;
        }
        cb(null, utils.bufferConcat(stdout));
    });
    child.stdin.end();
};

Repo.prototype.dirStat = function dirStat(ref, path, entries, cb) {
    var directories, files,
        commits = [],
        expected = entries.length;

    entries.forEach(function(entry) {
        this.stats(ref, entry, { maxcount: 1 }, function(err, commit) {
            if(err) throw err;
            commits.push(commit);

            if(commits.length == expected) {
                directories = commits.filter(function(a) {
                    return a.type == 'tree';
                });
                files = commits.filter(function(a) {
                    return a.type == 'blob';
                });
                directories.sort(sortByName);
                files.sort(sortByName);
                cb(null, directories.concat(files));
            }
        });
    }, this);
    return commits;
};

/**
 * If passed `str`, writes `str` to the repo description file, else reads the latter and returns it
 *
 * @param {String} str
 * @param {String} str
 * @api public
 * @return {String}
 */

Repo.prototype.description = function description(str) {
    var path = this.bare ? join(this.path, 'description') : join(this.path, '.git', 'description');
    if(str) {
        fs.writeFileSync(path, str);
    } else {
        return fs.readFileSync(path).toString();
    }
};

/**
 * Returns a Stat object for the given `entry` at the the given `ref`
 * Options are:
 *     - maxcount: Commit history to retrieve
 *     -
 *
 * @param {String} ref
 * @param {String, Object} entry
 * @param {Object} options
 * @api public
 */

Repo.prototype.stats = function stats(ref, entry, options /*, cb */) {
    options = options || { maxcount: -1, skip: 0 };

    if(typeof entry === 'string') entry = { path: entry };
    var cb  = (typeof options === 'function') ? options : arguments[3];

    this.gitExec(['log', ref,
        '--date=relative',
        '--max-count=' + (options.maxcount || -1),
        '--skip=' + (options.skip || 0), '--',entry.path
    ], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            entry.commits = new Commits(stdout);
            entry.tip = entry.commits.tip();
            entry.url = utils.url('', this.name, entry.type, ref, entry.path);
            return cb(null, entry);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, list) where `list` is an array containing commit objects for
 * each `entry` in the folder described by `path` for `ref`.
 *
 * @param {String} ref
 * @param {String} path
 * @param {Function} cb
 * @api public
 */

Repo.prototype.tree = function tree(ref, path, cb) {
    var entries;

    debug('tree for path "%s"', path);
    this.gitExec(['ls-tree', ref, path], function(err, stdout) {
        if(err) { return cb(err); }

        entries = this.parse(stdout);
        this.dirStat(ref, path, entries, function(err, tree) {
            if(err) { return cb(err); }
            cb(null, tree);
        });
    }.bind(this));
};

/**
 * Calls `cb` passing (err, buf) where `buf` is a buffer which is the content
 * of the file match by `path` for `ref`.
 *
 * @param {String} ref
 * @param {String} path
 * @param {Function} cb
 * @api public
 */

Repo.prototype.blob = function blob(ref, path, cb) {
    debug('blob for path "%s"', path);
    this.gitExec(['show', ref + ':' + path], cb);
};

/**
 * Calls `cb` passing (err, buf) where `buf` is a `format` archive of `ref` at its current state
 *
 * @param {String} ref
 * @param {String} format
 * @param {Function} cb
 * @api public
 */

Repo.prototype.archive = function archive(ref, format, cb) {
    debug('archiving ref "%s" in %s format', ref, format);
    this.gitExec(['archive', ref, '--format=' + format], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            return cb(null, stdout);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, list) where `list` is an array of the tags for this repo
 *
 * @param {Function} cb
 * @api public
 */

Repo.prototype.tags = function tags(cb) {
    var out;

    if(this.cache.has('tags')) {
        debug('[CACHE] tags: fetching from cache');
        return cb(null, this.cache.get('tags'));
    }

    this.gitExec(['tag', '-l'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            out = stdout.toString().split('\n').filter(utils.not(''));
            debug('[CACHE] tags: storing in cache, [%s]', out.join());
            this.cache.store('tags', out);
            return cb(null, out);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, list) where `list` is an array of the branches for this repo
 *
 * @param {Function} cb
 * @api public
 */

Repo.prototype.branches = function branches(cb) {
    var lines;

    if(this.cache.has('branches')) {
        debug('[CACHE] branches: fetching from cache');
        return cb(null, this.cache.get('branches'));
    }

    this.gitExec(['branch'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            lines = stdout.toString().split('\n');
            lines.pop();
            lines = lines.map(function(line) {
                return line.replace(/^\*\s/, '').trim();
            });
            debug('[CACHE] branches: storing in cache, [%s]', lines.join());
            this.cache.store('branches', lines);
            return cb(null, lines);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, obj) where `obj` is an hash, the last commit entry for `ref`
 *
 * @param {String} ref
 * @param {Function} cb
 * @api public
 */

Repo.prototype.tip = function tip(ref, cb) {
    var c;

    if(this.cache.has('tip:' + ref)) {
        debug('[CACHE] tip: fetching [%s] from cache', ref);
        return cb(null, this.cache.get('tip:' + ref));
    }

    this.gitExec(['log', ref, '-n1', '--date=relative', '--', '.'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            c = new Commits(stdout);
            debug('[CACHE] tip: storing [%s] in cache', ref);
            this.cache.store('tip:' + ref, c.tip());
            return cb(null, c.tip());
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, num) where `num` is the number of commits in the given `ref`
 * `num` is 0 if something horrible happened
 *
 * @param {String} ref
 * @param {Function} cb
 * @api public
 */

Repo.prototype.total = function total(ref, cb) {
    var num = 0;
    var global = /^.*\((\d+)\)\:/gm;
    var number = /\((\d+)\)/;

    if(this.cache.has('total:' + ref)) {
        debug('[CACHE] total: fetching [%s] from cache', ref);
        return cb(null, this.cache.get('total:' + ref));
    }

    this.gitExec(['shortlog', ref, '--'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            var str = stdout.toString();
            var matches = str.match(global);
            if(matches) {
                num = matches.reduce(function(memo, value) {
                    var num = value.match(number);
                    return memo + parseInt(num[1], 10);
                }, 0);
            }
            debug('[CACHE] total: storing [%s] in cache', ref);
            this.cache.store('total:' + ref, num);
            return cb(null, num);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, date) where `date` is a Date instance
 *
 * @param {Function} cb
 * @api public
 */

Repo.prototype.mtime = function mtime(cb) {
    if(this.cache.has('mtime')) {
        debug('[CACHE] mtime: fetching from cache');
        return cb(null, this.cache.get('mtime'));
    }

    fs.stat(this.path, function(err, stat) {
        if(err) {
            return cb(err);
        } else {
            debug('[CACHE] mtime: storing in cache');
            this.cache.store('mtime', stat.mtime);
            return cb(null, stat.mtime);
        }
    }.bind(this));
};
<commit_msg>Changed debug display<commit_after>"use strict";

var cp = require('child_process');
var fs = require('fs');

var Commits = require('./commits');
var Cache = require('./cache');
var utils = require('./utils');

var spawn = cp.spawn;
var basename = require('path').basename;
var join = require('path').join;
var debug = require('debug')('pushhub');

function sortByName(a, b) {
    if(a.name < b.name) return -1;
    if(a.name === b.name) return 0;
    if(a.name > b.name) return 1;
}

/**
 * Initialize a new `Repo` with the given `path`
 *
 * @param {String} path
 * @api public
 */

var Repo = module.exports = function Repo(path) {
    this.name = basename(path);
    this.path = path;
    this.cache = new Cache();
    this.bare = !utils.isDirectory(join(path, '.git'));
};

/**
 * Parse text coming from `git ls-tree` into an `Array` of `Object`s
 *
 * @param {String} text
 * @api private
 * @return {Array}
 */

Repo.prototype.parse = function parse(text) {
    text = text.toString();
    return text.split('\n').map(function(line) {
        var o = {
            'mode': line.slice(0, 6),
            'type': line.slice(7, 11),
            'hash': line.slice(12, 52),
            'path': line.slice(53)
        };
        // Useful to distinguish folders from revision/refs
        o.path += (o.type == 'tree') ? '/' : '';
        o.name = basename(o.path);
        return o;
    }, this);
};

/**
 * Proxy method to git commands, we don't use cp.exec here because the
 * stdout it returns is a `String` as it cause problems with binary
 * files
 *
 * @param {Array} commands
 * @param {Function} cb
 * @api private
 */

Repo.prototype.gitExec = function gitExec(commands, cb) {
    var child = spawn("git", commands, {cwd: this.path});
    var stdout = [], stderr = [];

    debug('[SPAWN] %s', "git " + commands.join(" "));
    child.stdout.on('data', function (text) {
        stdout[stdout.length] = text;
    });
    child.stderr.on('data', function (text) {
        stderr[stderr.length] = text;
    });
    child.on('close', function (code) {
        if (code > 0) {
            var err = new Error("git " + commands.join(" ") + "\n" + utils.bufferConcat(stderr, 'utf8'));
            cb(err);
            return;
        }
        cb(null, utils.bufferConcat(stdout));
    });
    child.stdin.end();
};

Repo.prototype.dirStat = function dirStat(ref, path, entries, cb) {
    var directories, files,
        commits = [],
        expected = entries.length;

    entries.forEach(function(entry) {
        this.stats(ref, entry, { maxcount: 1 }, function(err, commit) {
            if(err) throw err;
            commits.push(commit);

            if(commits.length == expected) {
                directories = commits.filter(function(a) {
                    return a.type == 'tree';
                });
                files = commits.filter(function(a) {
                    return a.type == 'blob';
                });
                directories.sort(sortByName);
                files.sort(sortByName);
                cb(null, directories.concat(files));
            }
        });
    }, this);
    return commits;
};

/**
 * If passed `str`, writes `str` to the repo description file, else reads the latter and returns it
 *
 * @param {String} str
 * @param {String} str
 * @api public
 * @return {String}
 */

Repo.prototype.description = function description(str) {
    var path = this.bare ? join(this.path, 'description') : join(this.path, '.git', 'description');
    if(str) {
        fs.writeFileSync(path, str);
    } else {
        return fs.readFileSync(path).toString();
    }
};

/**
 * Returns a Stat object for the given `entry` at the the given `ref`
 * Options are:
 *     - maxcount: Commit history to retrieve
 *     -
 *
 * @param {String} ref
 * @param {String, Object} entry
 * @param {Object} options
 * @api public
 */

Repo.prototype.stats = function stats(ref, entry, options /*, cb */) {
    options = options || { maxcount: -1, skip: 0 };

    if(typeof entry === 'string') entry = { path: entry };
    var cb  = (typeof options === 'function') ? options : arguments[3];

    this.gitExec(['log', ref,
        '--date=relative',
        '--max-count=' + (options.maxcount || -1),
        '--skip=' + (options.skip || 0), '--',entry.path
    ], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            entry.commits = new Commits(stdout);
            entry.tip = entry.commits.tip();
            entry.url = utils.url('', this.name, entry.type, ref, entry.path);
            return cb(null, entry);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, list) where `list` is an array containing commit objects for
 * each `entry` in the folder described by `path` for `ref`.
 *
 * @param {String} ref
 * @param {String} path
 * @param {Function} cb
 * @api public
 */

Repo.prototype.tree = function tree(ref, path, cb) {
    var entries;

    debug('tree for path "%s"', path);
    this.gitExec(['ls-tree', ref, path], function(err, stdout) {
        if(err) { return cb(err); }

        entries = this.parse(stdout);
        this.dirStat(ref, path, entries, function(err, tree) {
            if(err) { return cb(err); }
            cb(null, tree);
        });
    }.bind(this));
};

/**
 * Calls `cb` passing (err, buf) where `buf` is a buffer which is the content
 * of the file match by `path` for `ref`.
 *
 * @param {String} ref
 * @param {String} path
 * @param {Function} cb
 * @api public
 */

Repo.prototype.blob = function blob(ref, path, cb) {
    debug('blob for path "%s"', path);
    this.gitExec(['show', ref + ':' + path], cb);
};

/**
 * Calls `cb` passing (err, buf) where `buf` is a `format` archive of `ref` at its current state
 *
 * @param {String} ref
 * @param {String} format
 * @param {Function} cb
 * @api public
 */

Repo.prototype.archive = function archive(ref, format, cb) {
    debug('archiving ref "%s" in %s format', ref, format);
    this.gitExec(['archive', ref, '--format=' + format], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            return cb(null, stdout);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, list) where `list` is an array of the tags for this repo
 *
 * @param {Function} cb
 * @api public
 */

Repo.prototype.tags = function tags(cb) {
    var out;

    if(this.cache.has('tags')) {
        debug('[CACHE] tags: fetching from cache');
        return cb(null, this.cache.get('tags'));
    }

    this.gitExec(['tag', '-l'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            out = stdout.toString().split('\n').filter(utils.not(''));
            debug('[CACHE] tags: storing in cache, [%s]', out.join());
            this.cache.store('tags', out);
            return cb(null, out);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, list) where `list` is an array of the branches for this repo
 *
 * @param {Function} cb
 * @api public
 */

Repo.prototype.branches = function branches(cb) {
    var lines;

    if(this.cache.has('branches')) {
        debug('[CACHE] branches: fetching from cache');
        return cb(null, this.cache.get('branches'));
    }

    this.gitExec(['branch'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            lines = stdout.toString().split('\n');
            lines.pop();
            lines = lines.map(function(line) {
                return line.replace(/^\*\s/, '').trim();
            });
            debug('[CACHE] branches: storing in cache, [%s]', lines.join());
            this.cache.store('branches', lines);
            return cb(null, lines);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, obj) where `obj` is an hash, the last commit entry for `ref`
 *
 * @param {String} ref
 * @param {Function} cb
 * @api public
 */

Repo.prototype.tip = function tip(ref, cb) {
    var c;

    if(this.cache.has('tip:' + ref)) {
        debug('[CACHE] tip: fetching [%s] from cache', ref);
        return cb(null, this.cache.get('tip:' + ref));
    }

    this.gitExec(['log', ref, '-n1', '--date=relative', '--', '.'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            c = new Commits(stdout);
            debug('[CACHE] tip: storing [%s] in cache', ref);
            this.cache.store('tip:' + ref, c.tip());
            return cb(null, c.tip());
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, num) where `num` is the number of commits in the given `ref`
 * `num` is 0 if something horrible happened
 *
 * @param {String} ref
 * @param {Function} cb
 * @api public
 */

Repo.prototype.total = function total(ref, cb) {
    var num = 0;
    var global = /^.*\((\d+)\)\:/gm;
    var number = /\((\d+)\)/;

    if(this.cache.has('total:' + ref)) {
        debug('[CACHE] total: fetching [%s] from cache', ref);
        return cb(null, this.cache.get('total:' + ref));
    }

    this.gitExec(['shortlog', ref, '--'], function(err, stdout) {
        if(err) {
            return cb(err);
        } else {
            var str = stdout.toString();
            var matches = str.match(global);
            if(matches) {
                num = matches.reduce(function(memo, value) {
                    var num = value.match(number);
                    return memo + parseInt(num[1], 10);
                }, 0);
            }
            debug('[CACHE] total: storing [%s] in cache', ref);
            this.cache.store('total:' + ref, num);
            return cb(null, num);
        }
    }.bind(this));
};

/**
 * Calls `cb` passing (err, date) where `date` is a Date instance
 *
 * @param {Function} cb
 * @api public
 */

Repo.prototype.mtime = function mtime(cb) {
    if(this.cache.has('mtime')) {
        debug('[CACHE] mtime: fetching from cache');
        return cb(null, this.cache.get('mtime'));
    }

    fs.stat(this.path, function(err, stat) {
        if(err) {
            return cb(err);
        } else {
            debug('[CACHE] mtime: storing in cache');
            this.cache.store('mtime', stat.mtime);
            return cb(null, stat.mtime);
        }
    }.bind(this));
};
<|endoftext|>