<commit_before>import { expect } from 'chai';
import sinon from 'sinon';

import models from '../../../server/models';
import { fakeCollective, fakeHost, fakeOrder, fakeUser } from '../../test-helpers/fake-data';
import * as utils from '../../utils';

const { Transaction } = models;

const transactionsData = utils.data('transactions1').transactions;

describe('server/models/Transaction', () => {
  let user, host, inc, collective, defaultTransactionData;

  beforeEach(() => utils.resetTestDB());

  beforeEach(async () => {
    user = await fakeUser({}, { id: 10 });
    inc = await fakeHost({
      id: 8686,
      slug: 'opencollectiveinc',
      CreatedByUserId: user.id,
      HostCollectiveId: 8686,
    });
    host = await fakeHost({ id: 2, CreatedByUserId: user.id });
    collective = await fakeCollective({ id: 3, HostCollectiveId: host.id, CreatedByUserId: user.id });
    defaultTransactionData = {
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
      HostCollectiveId: host.id,
    };
  });

  it('automatically generates uuid', done => {
    Transaction.create({
      amount: -1000,
      ...defaultTransactionData,
    })
      .then(transaction => {
        expect(transaction.info.uuid).to.match(
          /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
        );
        done();
      })
      .catch(done);
  });

  it('get the host', done => {
    Transaction.create({
      ...defaultTransactionData,
      amount: 10000,
    }).then(transaction => {
      expect(transaction.HostCollectiveId).to.equal(host.id);
      done();
    });
  });

  it('createFromPayload creates a double entry transaction for a Stripe payment in EUR with VAT', () => {
    const transaction = {
      description: '€121 for Vegan Burgers including €21 VAT',
      amount: 12100,
      amountInHostCurrency: 12100,
      currency: 'EUR',
      hostCurrency: 'EUR',
      hostCurrencyFxRate: 1,
      platformFeeInHostCurrency: 500,
      hostFeeInHostCurrency: 500,
      paymentProcessorFeeInHostCurrency: 300,
      taxAmount: 2100,
      type: 'CREDIT',
      createdAt: '2015-05-29T07:00:00.000Z',
      PaymentMethodId: 1,
    };

    return Transaction.createFromPayload({
      transaction,
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    }).then(() => {
      return Transaction.findAll().then(transactions => {
        expect(transactions.length).to.equal(2);
        expect(transactions[0].type).to.equal('DEBIT');
        expect(transactions[0].netAmountInCollectiveCurrency).to.equal(-12100);
        expect(transactions[0].currency).to.equal('EUR');
        expect(transactions[0].HostCollectiveId).to.be.null;

        expect(transactions[1].type).to.equal('CREDIT');
        expect(transactions[1].amount).to.equal(12100);
        expect(transactions[1].platformFeeInHostCurrency).to.equal(-500);
        expect(transactions[1].paymentProcessorFeeInHostCurrency).to.equal(-300);
        expect(transactions[1].taxAmount).to.equal(-2100);
        expect(transactions[1].amount).to.equal(12100);
        expect(transactions[1].netAmountInCollectiveCurrency).to.equal(8700);
        expect(transactions[0] instanceof models.Transaction).to.be.true;
        expect(transactions[0].description).to.equal(transaction.description);
      });
    });
  });

  it('createFromPayload creates a double entry transaction for a Stripe donation in EUR on a USD host', () => {
    const transaction = {
      description: '€100 donation to WWCode Berlin',
      amount: 10000,
      amountInHostCurrency: 11000,
      currency: 'EUR',
      hostCurrency: 'USD',
      hostCurrencyFxRate: 1.1,
      platformFeeInHostCurrency: 550,
      hostFeeInHostCurrency: 550,
      paymentProcessorFeeInHostCurrency: 330,
      type: 'CREDIT',
      createdAt: '2015-05-29T07:00:00.000Z',
      PaymentMethodId: 1,
    };

    return Transaction.createFromPayload({
      transaction,
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    }).then(() => {
      return Transaction.findAll().then(transactions => {
        expect(transactions.length).to.equal(2);
        expect(transactions[0].type).to.equal('DEBIT');
        expect(transactions[0].netAmountInCollectiveCurrency).to.equal(-10000);
        expect(transactions[0].currency).to.equal('EUR');
        expect(transactions[0].HostCollectiveId).to.be.null;

        expect(transactions[1].type).to.equal('CREDIT');
        expect(transactions[1].amount).to.equal(10000);
        expect(transactions[1].platformFeeInHostCurrency).to.equal(-550);
        expect(transactions[1].paymentProcessorFeeInHostCurrency).to.equal(-330);
        expect(transactions[1].taxAmount).to.be.null;
        expect(transactions[1].amount).to.equal(10000);
        expect(transactions[1].netAmountInCollectiveCurrency).to.equal(8700);
        expect(transactions[0] instanceof models.Transaction).to.be.true;
        expect(transactions[0].description).to.equal(transaction.description);
      });
    });
  });

  it('createFromPayload() generates a new activity', done => {
    const createActivityStub = sinon.stub(Transaction, 'createActivity').callsFake(t => {
      expect(Math.abs(t.amount)).to.equal(Math.abs(transactionsData[7].netAmountInCollectiveCurrency));
      createActivityStub.restore();
      done();
    });

    Transaction.createFromPayload({
      transaction: transactionsData[7],
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    })
      .then(transaction => {
        expect(transaction.CollectiveId).to.equal(collective.id);
      })
      .catch(done);
  });

  describe('fees on top', () => {
    it('should deduct the platform fee from the main transactions', async () => {
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 11000,
        amountInHostCurrency: 11000,
        currency: 'USD',
        hostCurrency: 'USD',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 1000,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 300,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        data: {
          isFeesOnTop: true,
        },
      };

      const t = await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      expect(t).to.have.property('platformFeeInHostCurrency').equal(0);
      expect(t)
        .to.have.property('netAmountInCollectiveCurrency')
        .equal(
          // The total amount of donation minus the fees on top
          10000 -
            // Minus the host fee
            500 -
            // Minus the partial platform fee: (10000 out of 11000)
            Math.round((300 * 10000) / 11000),
        );
    });

    it('should create an aditional pair of transactions between contributor and Open Collective Inc', async () => {
      const order = await fakeOrder({
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 11000,
        totalAmount: 11000,
        amountInHostCurrency: 11000,
        currency: 'USD',
        hostCurrency: 'USD',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 1000,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 200,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        OrderId: order.id,
        data: {
          isFeesOnTop: true,
        },
      };

      const createdTransaction = await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      const allTransactions = await Transaction.findAll({ where: { OrderId: order.id } });
      expect(allTransactions).to.have.length(4);

      const donationCredit = allTransactions.find(t => t.CollectiveId === inc.id);
      expect(donationCredit).to.have.property('type').equal('CREDIT');
      expect(donationCredit).to.have.property('amount').equal(1000);
      expect(donationCredit)
        .to.have.property('PlatformTipForTransactionGroup')
        .equal(createdTransaction.TransactionGroup);

      const donationDebit = allTransactions.find(t => t.FromCollectiveId === inc.id);
      const partialPaymentProcessorFee = Math.round(200 * (1000 / 11000));
      expect(donationDebit).to.have.property('type').equal('DEBIT');
      expect(donationDebit)
        .to.have.property('amount')
        .equal(-1000 + partialPaymentProcessorFee);
      expect(donationDebit)
        .to.have.property('PlatformTipForTransactionGroup')
        .equal(createdTransaction.TransactionGroup);
    });

    it('should convert the donation transaction to USD and store the FX rate', async () => {
      const order = await fakeOrder({
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
        currency: 'EUR',
      });
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 11000,
        totalAmount: 11000,
        amountInHostCurrency: 11000,
        currency: 'EUR',
        hostCurrency: 'EUR',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 1000,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 200,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        OrderId: order.id,
        data: {
          isFeesOnTop: true,
        },
      };

      await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      const allTransactions = await Transaction.findAll({ where: { OrderId: order.id } });
      expect(allTransactions).to.have.length(4);

      const donationCredit = allTransactions.find(t => t.CollectiveId === inc.id);
      expect(donationCredit).to.have.property('type').equal('CREDIT');
      expect(donationCredit).to.have.property('currency').equal('USD');
      expect(donationCredit).to.have.nested.property('data.hostToPlatformFxRate');
      expect(donationCredit)
        .to.have.property('amount')
        .equal(Math.round(1000 * donationCredit.data.hostToPlatformFxRate));

      const donationDebit = allTransactions.find(t => t.FromCollectiveId === inc.id);
      const partialPaymentProcessorFee = Math.round(200 * (1000 / 11000));
      expect(donationDebit).to.have.nested.property('data.hostToPlatformFxRate');
      expect(donationDebit).to.have.property('type').equal('DEBIT');
      expect(donationDebit).to.have.property('currency').equal('USD');
      expect(donationDebit)
        .to.have.property('amount')
        .equal(Math.round((-1000 + partialPaymentProcessorFee) * donationDebit.data.hostToPlatformFxRate));
    });

    it('should not create transactions if platformFee is 0', async () => {
      const order = await fakeOrder({
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
        currency: 'EUR',
      });
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 10000,
        totalAmount: 10000,
        amountInHostCurrency: 10000,
        currency: 'EUR',
        hostCurrency: 'EUR',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 0,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 200,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        OrderId: order.id,
        data: {
          isFeesOnTop: true,
        },
      };

      await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      const allTransactions = await Transaction.findAll({ where: { OrderId: order.id } });
      expect(allTransactions).to.have.length(2);
    });
  });

  it('should convert properly when using setCurrency', async () => {
    const order = await fakeOrder({
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
      currency: 'USD',
    });

    const transaction = {
      description: 'Financial contribution to Booky Foundation',
      amount: 500,
      currency: 'USD',
      amountInHostCurrency: 402,
      hostCurrency: 'EUR',
      hostCurrencyFxRate: 0.804,
      platformFeeInHostCurrency: 0,
      hostFeeInHostCurrency: 0,
      paymentProcessorFeeInHostCurrency: -31,
      type: 'CREDIT',
      PaymentMethodId: 1,
      OrderId: order.id,
      data: {
        charge: { currency: 'usd' },
        balanceTransaction: {
          currency: 'eur',
          exchange_rate: 0.803246, // eslint-disable-line camelcase
        },
      },
    };

    const credit = await Transaction.createFromPayload({
      transaction,
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    });

    await Transaction.validate(credit);

    await credit.setCurrency('EUR');

    await Transaction.validate(credit);

    expect(credit).to.have.property('currency').equal('EUR');

    expect(credit).to.have.property('amount').equal(402);
  });
});
<commit_msg>test: update tests to reflect payment processor fee reimbursement<commit_after>import { expect } from 'chai';
import sinon from 'sinon';

import models from '../../../server/models';
import { fakeCollective, fakeHost, fakeOrder, fakeUser } from '../../test-helpers/fake-data';
import * as utils from '../../utils';

const { Transaction } = models;

const transactionsData = utils.data('transactions1').transactions;

describe('server/models/Transaction', () => {
  let user, host, inc, collective, defaultTransactionData;

  beforeEach(() => utils.resetTestDB());

  beforeEach(async () => {
    user = await fakeUser({}, { id: 10 });
    inc = await fakeHost({
      id: 8686,
      slug: 'opencollectiveinc',
      CreatedByUserId: user.id,
      HostCollectiveId: 8686,
    });
    host = await fakeHost({ id: 2, CreatedByUserId: user.id, data: { reimbursePaymentProcessorFeeOnTips: true } });
    collective = await fakeCollective({ id: 3, HostCollectiveId: host.id, CreatedByUserId: user.id });
    defaultTransactionData = {
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
      HostCollectiveId: host.id,
    };
  });

  it('automatically generates uuid', done => {
    Transaction.create({
      amount: -1000,
      ...defaultTransactionData,
    })
      .then(transaction => {
        expect(transaction.info.uuid).to.match(
          /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
        );
        done();
      })
      .catch(done);
  });

  it('get the host', done => {
    Transaction.create({
      ...defaultTransactionData,
      amount: 10000,
    }).then(transaction => {
      expect(transaction.HostCollectiveId).to.equal(host.id);
      done();
    });
  });

  it('createFromPayload creates a double entry transaction for a Stripe payment in EUR with VAT', () => {
    const transaction = {
      description: '€121 for Vegan Burgers including €21 VAT',
      amount: 12100,
      amountInHostCurrency: 12100,
      currency: 'EUR',
      hostCurrency: 'EUR',
      hostCurrencyFxRate: 1,
      platformFeeInHostCurrency: 500,
      hostFeeInHostCurrency: 500,
      paymentProcessorFeeInHostCurrency: 300,
      taxAmount: 2100,
      type: 'CREDIT',
      createdAt: '2015-05-29T07:00:00.000Z',
      PaymentMethodId: 1,
    };

    return Transaction.createFromPayload({
      transaction,
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    }).then(() => {
      return Transaction.findAll().then(transactions => {
        expect(transactions.length).to.equal(2);
        expect(transactions[0].type).to.equal('DEBIT');
        expect(transactions[0].netAmountInCollectiveCurrency).to.equal(-12100);
        expect(transactions[0].currency).to.equal('EUR');
        expect(transactions[0].HostCollectiveId).to.be.null;

        expect(transactions[1].type).to.equal('CREDIT');
        expect(transactions[1].amount).to.equal(12100);
        expect(transactions[1].platformFeeInHostCurrency).to.equal(-500);
        expect(transactions[1].paymentProcessorFeeInHostCurrency).to.equal(-300);
        expect(transactions[1].taxAmount).to.equal(-2100);
        expect(transactions[1].amount).to.equal(12100);
        expect(transactions[1].netAmountInCollectiveCurrency).to.equal(8700);
        expect(transactions[0] instanceof models.Transaction).to.be.true;
        expect(transactions[0].description).to.equal(transaction.description);
      });
    });
  });

  it('createFromPayload creates a double entry transaction for a Stripe donation in EUR on a USD host', () => {
    const transaction = {
      description: '€100 donation to WWCode Berlin',
      amount: 10000,
      amountInHostCurrency: 11000,
      currency: 'EUR',
      hostCurrency: 'USD',
      hostCurrencyFxRate: 1.1,
      platformFeeInHostCurrency: 550,
      hostFeeInHostCurrency: 550,
      paymentProcessorFeeInHostCurrency: 330,
      type: 'CREDIT',
      createdAt: '2015-05-29T07:00:00.000Z',
      PaymentMethodId: 1,
    };

    return Transaction.createFromPayload({
      transaction,
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    }).then(() => {
      return Transaction.findAll().then(transactions => {
        expect(transactions.length).to.equal(2);
        expect(transactions[0].type).to.equal('DEBIT');
        expect(transactions[0].netAmountInCollectiveCurrency).to.equal(-10000);
        expect(transactions[0].currency).to.equal('EUR');
        expect(transactions[0].HostCollectiveId).to.be.null;

        expect(transactions[1].type).to.equal('CREDIT');
        expect(transactions[1].amount).to.equal(10000);
        expect(transactions[1].platformFeeInHostCurrency).to.equal(-550);
        expect(transactions[1].paymentProcessorFeeInHostCurrency).to.equal(-330);
        expect(transactions[1].taxAmount).to.be.null;
        expect(transactions[1].amount).to.equal(10000);
        expect(transactions[1].netAmountInCollectiveCurrency).to.equal(8700);
        expect(transactions[0] instanceof models.Transaction).to.be.true;
        expect(transactions[0].description).to.equal(transaction.description);
      });
    });
  });

  it('createFromPayload() generates a new activity', done => {
    const createActivityStub = sinon.stub(Transaction, 'createActivity').callsFake(t => {
      expect(Math.abs(t.amount)).to.equal(Math.abs(transactionsData[7].netAmountInCollectiveCurrency));
      createActivityStub.restore();
      done();
    });

    Transaction.createFromPayload({
      transaction: transactionsData[7],
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    })
      .then(transaction => {
        expect(transaction.CollectiveId).to.equal(collective.id);
      })
      .catch(done);
  });

  describe('fees on top', () => {
    it('should deduct the platform fee from the main transactions', async () => {
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 11000,
        amountInHostCurrency: 11000,
        currency: 'USD',
        hostCurrency: 'USD',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 1000,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 300,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        data: {
          isFeesOnTop: true,
        },
      };

      const t = await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      expect(t).to.have.property('platformFeeInHostCurrency').equal(0);
      expect(t)
        .to.have.property('netAmountInCollectiveCurrency')
        .equal(
          // The total amount of donation minus the fees on top
          10000 -
            // Minus the host fee
            500 -
            // Minus the partial platform fee: (10000 out of 11000)
            Math.round((300 * 10000) / 11000),
        );
    });

    it('should create an aditional pair of transactions between contributor and Open Collective Inc', async () => {
      const order = await fakeOrder({
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 11000,
        totalAmount: 11000,
        amountInHostCurrency: 11000,
        currency: 'USD',
        hostCurrency: 'USD',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 1000,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 200,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        OrderId: order.id,
        data: {
          isFeesOnTop: true,
        },
      };

      const createdTransaction = await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      const allTransactions = await Transaction.findAll({ where: { OrderId: order.id } });
      expect(allTransactions).to.have.length(4);

      const donationCredit = allTransactions.find(t => t.CollectiveId === inc.id);
      expect(donationCredit).to.have.property('type').equal('CREDIT');
      expect(donationCredit).to.have.property('amount').equal(1000);
      expect(donationCredit)
        .to.have.property('PlatformTipForTransactionGroup')
        .equal(createdTransaction.TransactionGroup);

      const donationDebit = allTransactions.find(t => t.FromCollectiveId === inc.id);
      const partialPaymentProcessorFee = Math.round(200 * (1000 / 11000));
      expect(donationDebit).to.have.property('type').equal('DEBIT');
      expect(donationDebit)
        .to.have.property('amount')
        .equal(-1000 + partialPaymentProcessorFee);
      expect(donationDebit)
        .to.have.property('PlatformTipForTransactionGroup')
        .equal(createdTransaction.TransactionGroup);
    });

    it('should convert the donation transaction to USD and store the FX rate', async () => {
      const order = await fakeOrder({
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
        currency: 'EUR',
      });
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 11000,
        totalAmount: 11000,
        amountInHostCurrency: 11000,
        currency: 'EUR',
        hostCurrency: 'EUR',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 1000,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 200,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        OrderId: order.id,
        data: {
          isFeesOnTop: true,
        },
      };

      await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      const allTransactions = await Transaction.findAll({ where: { OrderId: order.id } });
      expect(allTransactions).to.have.length(4);

      const donationCredit = allTransactions.find(t => t.CollectiveId === inc.id);
      expect(donationCredit).to.have.property('type').equal('CREDIT');
      expect(donationCredit).to.have.property('currency').equal('USD');
      expect(donationCredit).to.have.nested.property('data.hostToPlatformFxRate');
      expect(donationCredit)
        .to.have.property('amount')
        .equal(Math.round(1000 * donationCredit.data.hostToPlatformFxRate));

      const donationDebit = allTransactions.find(t => t.FromCollectiveId === inc.id);
      const partialPaymentProcessorFee = Math.round(200 * (1000 / 11000));
      expect(donationDebit).to.have.nested.property('data.hostToPlatformFxRate');
      expect(donationDebit).to.have.property('type').equal('DEBIT');
      expect(donationDebit).to.have.property('currency').equal('USD');
      expect(donationDebit)
        .to.have.property('amount')
        .equal(Math.round((-1000 + partialPaymentProcessorFee) * donationDebit.data.hostToPlatformFxRate));
    });

    it('should not create transactions if platformFee is 0', async () => {
      const order = await fakeOrder({
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
        currency: 'EUR',
      });
      const transaction = {
        description: '$100 donation to Merveilles',
        amount: 10000,
        totalAmount: 10000,
        amountInHostCurrency: 10000,
        currency: 'EUR',
        hostCurrency: 'EUR',
        hostCurrencyFxRate: 1,
        platformFeeInHostCurrency: 0,
        hostFeeInHostCurrency: 500,
        paymentProcessorFeeInHostCurrency: 200,
        type: 'CREDIT',
        createdAt: '2015-05-29T07:00:00.000Z',
        PaymentMethodId: 1,
        OrderId: order.id,
        data: {
          isFeesOnTop: true,
        },
      };

      await Transaction.createFromPayload({
        transaction,
        CreatedByUserId: user.id,
        FromCollectiveId: user.CollectiveId,
        CollectiveId: collective.id,
      });

      const allTransactions = await Transaction.findAll({ where: { OrderId: order.id } });
      expect(allTransactions).to.have.length(2);
    });
  });

  it('should convert properly when using setCurrency', async () => {
    const order = await fakeOrder({
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
      currency: 'USD',
    });

    const transaction = {
      description: 'Financial contribution to Booky Foundation',
      amount: 500,
      currency: 'USD',
      amountInHostCurrency: 402,
      hostCurrency: 'EUR',
      hostCurrencyFxRate: 0.804,
      platformFeeInHostCurrency: 0,
      hostFeeInHostCurrency: 0,
      paymentProcessorFeeInHostCurrency: -31,
      type: 'CREDIT',
      PaymentMethodId: 1,
      OrderId: order.id,
      data: {
        charge: { currency: 'usd' },
        balanceTransaction: {
          currency: 'eur',
          exchange_rate: 0.803246, // eslint-disable-line camelcase
        },
      },
    };

    const credit = await Transaction.createFromPayload({
      transaction,
      CreatedByUserId: user.id,
      FromCollectiveId: user.CollectiveId,
      CollectiveId: collective.id,
    });

    await Transaction.validate(credit);

    await credit.setCurrency('EUR');

    await Transaction.validate(credit);

    expect(credit).to.have.property('currency').equal('EUR');

    expect(credit).to.have.property('amount').equal(402);
  });
});
<|endoftext|>


<commit_before>/*
 * This is an internal configuration file.
 * If you want to change some configurable values, use the '/customize/application_config.js'
 * file (make a copy from /customize.dist/application_config.js)
 */
define(function() {
    var config = {};

    /* Select the buttons displayed on the main page to create new collaborative sessions.
     * Removing apps from the list will prevent users from accessing them. They will instead be
     * redirected to the drive.
     * You should never remove the drive from this list.
     */
    config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
                                /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'calendar'];
    /* The registered only types are apps restricted to registered users.
     * You should never remove apps from this list unless you know what you're doing. The apps
     * listed here by default can't work without a user account.
     * You can however add apps to this list. The new apps won't be visible for unregistered
     * users and these users will be redirected to the login page if they still try to access
     * the app
     */
    config.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support', 'calendar'];

    /* CryptPad is available is multiple languages, but only English and French are maintained
     * by the developers. The other languages may be outdated, and any missing string for a langauge
     * will use the english version instead. You can customize the langauges you want to be available
     * on your instance by removing them from the following list.
     * An empty list will load all available languages for CryptPad. The list of available languages
     * can be found at the top of the file `/customize.dist/messages.js`. The list should only
     * contain languages code ('en', 'fr', 'de', 'pt-br', etc.), not their full name.
     */
    //config.availableLanguages = ['en', 'fr', 'de'];

    /* You can display a link to the imprint (legal notice) of your website in the static pages
     * footer. To do so, you can either set the following value to `true` and create an imprint.html page
     * in the `customize` directory. You can also set it to an absolute URL if your imprint page already exists.
     */
    config.imprint = false;
    // config.imprint = true;
    // config.imprint = 'https://xwiki.com/en/company/legal-notice';

    /* You can display a link to your own privacy policy in the static pages footer.
     * To do so, set the following value to the absolute URL of your privacy policy.
     */
    // config.privacy = 'https://xwiki.com/en/company/PrivacyPolicy';

    /*  Cryptpad apps use a common API to display notifications to users
     *  by default, notifications are hidden after 5 seconds
     *  You can change their duration here (measured in milliseconds)
     */
    config.notificationTimeout = 5000;
    config.disableUserlistNotifications = false;

    // Update the default colors available in the whiteboard application
    config.whiteboardPalette = [
        '#000000', // black
        '#FFFFFF', // white
        '#848484', // grey
        '#8B4513', // saddlebrown
        '#FF0000', // red
        '#FF8080', // peach?
        '#FF8000', // orange
        '#FFFF00', // yellow
        '#80FF80', // light green
        '#00FF00', // green
        '#00FFFF', // cyan
        '#008B8B', // dark cyan
        '#0000FF', // blue
        '#FF00FF', // fuschia
        '#FF00C0', // hot pink
        '#800080', // purple
    ];

    // Background color in the apps with centered content:
    // - file app in view mode
    // - rich text app when editor's width reduced in settings
    config.appBackgroundColor = '#666';

    // Set enableTemplates to false to remove the button allowing users to save a pad as a template
    // and remove the template category in CryptDrive
    config.enableTemplates = true;

    // Set enableHistory to false to remove the "History" button in all the apps.
    config.enableHistory = true;

    /*  user passwords are hashed with scrypt, and salted with their username.
        this value will be appended to the username, causing the resulting hash
        to differ from other CryptPad instances if customized. This makes it
        such that anyone who wants to bruteforce common credentials must do so
        again on each CryptPad instance that they wish to attack.

        WARNING: this should only be set when your CryptPad instance is first
        created. Changing it at a later time will break logins for all existing
        users.
    */
    config.loginSalt = '';
    config.minimumPasswordLength = 8;

    // Amount of time (ms) before aborting the session when the algorithm cannot synchronize the pad
    config.badStateTimeout = 30000;

    // Customize the icon used for each application.
    // You can update the colors by making a copy of /customize.dist/src/less2/include/colortheme.less
    config.applicationsIcon = {
        file: 'cptools-file',
        fileupload: 'cptools-file-upload',
        folderupload: 'cptools-folder-upload',
        pad: 'cptools-richtext',
        code: 'cptools-code',
        slide: 'cptools-slide',
        poll: 'cptools-poll',
        whiteboard: 'cptools-whiteboard',
        todo: 'cptools-todo',
        contacts: 'fa-address-book',
        kanban: 'cptools-kanban',
        doc: 'fa-file-word-o',
        presentation: 'fa-file-powerpoint-o',
        sheet: 'cptools-sheet',
        drive: 'fa-hdd-o',
        teams: 'fa-users',
    };

    // Ability to create owned pads and expiring pads through a new pad creation screen.
    // The new screen can be disabled by the users in their settings page
    config.displayCreationScreen = true;

    // Prevent anonymous users from storing pads in their drive
    config.disableAnonymousStore = false;

    // Hide the usage bar in settings and drive
    //config.hideUsageBar = true;

    // Disable feedback for all the users and hide the settings part about feedback
    //config.disableFeedback = true;

    // Add new options in the share modal (extend an existing tab or add a new tab).
    // More info about how to use it on the wiki:
    // https://github.com/xwiki-labs/cryptpad/wiki/Application-config#configcustomizeshareoptions
    //config.customizeShareOptions = function (hashes, tabs, config) {};

    // Add code to be executed on every page before loading the user object. `isLoggedIn` (bool) is
    // indicating if the user is registered or anonymous. Here you can change the way anonymous users
    // work in CryptPad, use an external SSO or even force registration
    // *NOTE*: You have to call the `callback` function to continue the loading process
    //config.beforeLogin = function(isLoggedIn, callback) {};

    // Add code to be executed on every page after the user object is loaded (also work for
    // unregistered users). This allows you to interact with your users' drive
    // *NOTE*: You have to call the `callback` function to continue the loading process
    //config.afterLogin = function(api, callback) {};

    // Disabling the profile app allows you to import the profile informations (display name, avatar)
    // from an external source and make sure the users can't change them from CryptPad.
    // You can use config.afterLogin to import these values in the users' drive.
    //config.disableProfile = true;

    // Disable the use of webworkers and sharedworkers in CryptPad.
    // Workers allow us to run the websockets connection and open the user drive in a separate thread.
    // SharedWorkers allow us to load only one websocket and one user drive for all the browser tabs,
    // making it much faster to open new tabs.
    config.disableWorkers = false;

    // Teams are always loaded during the initial loading screen (for the first tab only if
    // SharedWorkers are available). Allowing users to be members of multiple teams can
    // make them have a very slow loading time. To avoid impacting the user experience
    // significantly, we're limiting the number of teams per user to 3 by default.
    // You can change this value here.
    //config.maxTeamsSlots = 5;

    // Each team is considered as a registered user by the server. Users and teams are indistinguishable
    // in the database so teams will offer the same storage limits as users by default.
    // It means that each team created by a user can increase their storage limit by +100%.
    // We're limiting the number of teams each user is able to own to 1 in order to make sure
    // users don't use "fake" teams (1 member) just to increase their storage limit.
    // You can change the value here.
    // config.maxOwnedTeams = 5;

    // The userlist displayed in collaborative documents is stored alongside the document data.
    // Everytime someone with edit rights joins a document or modify their user data (display
    // name, avatar, color, etc.), they update the "userlist" part of the document. When too many
    // editors are in the same document, all these changes increase the risks of conflicts which
    // require CPU time to solve. A "degraded" mode can now be set when a certain number of editors
    // are in a document at the same time. This mode disables the userlist, the chat and the
    // position of other users' cursor. You can configure the number of user from which the session
    // will enter into degraded mode. A big number may result in collaborative edition being broken,
    // but this number depends on the network and CPU performances of each user's device.
    config.degradedLimit = 8;

    // In "legacy" mode, one-time users were always creating an "anonymous" drive when visiting CryptPad
    // in which they could store their pads. The new "driveless" mode allow users to open an existing
    // pad without creating a drive in the background. The drive will only be created if they visit
    // a different page (Drive, Settings, etc.) or try to create a new pad themselves. You can disable
    // the driveless mode by changing the following value to "false"
    config.allowDrivelessMode = true;

    return config;
});
<commit_msg>remove calendar from app menu<commit_after>/*
 * This is an internal configuration file.
 * If you want to change some configurable values, use the '/customize/application_config.js'
 * file (make a copy from /customize.dist/application_config.js)
 */
define(function() {
    var config = {};

    /* Select the buttons displayed on the main page to create new collaborative sessions.
     * Removing apps from the list will prevent users from accessing them. They will instead be
     * redirected to the drive.
     * You should never remove the drive from this list.
     */
    config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
                                /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts' /*, 'calendar' */];
    /* The registered only types are apps restricted to registered users.
     * You should never remove apps from this list unless you know what you're doing. The apps
     * listed here by default can't work without a user account.
     * You can however add apps to this list. The new apps won't be visible for unregistered
     * users and these users will be redirected to the login page if they still try to access
     * the app
     */
    config.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support', 'calendar'];

    /* CryptPad is available is multiple languages, but only English and French are maintained
     * by the developers. The other languages may be outdated, and any missing string for a langauge
     * will use the english version instead. You can customize the langauges you want to be available
     * on your instance by removing them from the following list.
     * An empty list will load all available languages for CryptPad. The list of available languages
     * can be found at the top of the file `/customize.dist/messages.js`. The list should only
     * contain languages code ('en', 'fr', 'de', 'pt-br', etc.), not their full name.
     */
    //config.availableLanguages = ['en', 'fr', 'de'];

    /* You can display a link to the imprint (legal notice) of your website in the static pages
     * footer. To do so, you can either set the following value to `true` and create an imprint.html page
     * in the `customize` directory. You can also set it to an absolute URL if your imprint page already exists.
     */
    config.imprint = false;
    // config.imprint = true;
    // config.imprint = 'https://xwiki.com/en/company/legal-notice';

    /* You can display a link to your own privacy policy in the static pages footer.
     * To do so, set the following value to the absolute URL of your privacy policy.
     */
    // config.privacy = 'https://xwiki.com/en/company/PrivacyPolicy';

    /*  Cryptpad apps use a common API to display notifications to users
     *  by default, notifications are hidden after 5 seconds
     *  You can change their duration here (measured in milliseconds)
     */
    config.notificationTimeout = 5000;
    config.disableUserlistNotifications = false;

    // Update the default colors available in the whiteboard application
    config.whiteboardPalette = [
        '#000000', // black
        '#FFFFFF', // white
        '#848484', // grey
        '#8B4513', // saddlebrown
        '#FF0000', // red
        '#FF8080', // peach?
        '#FF8000', // orange
        '#FFFF00', // yellow
        '#80FF80', // light green
        '#00FF00', // green
        '#00FFFF', // cyan
        '#008B8B', // dark cyan
        '#0000FF', // blue
        '#FF00FF', // fuschia
        '#FF00C0', // hot pink
        '#800080', // purple
    ];

    // Background color in the apps with centered content:
    // - file app in view mode
    // - rich text app when editor's width reduced in settings
    config.appBackgroundColor = '#666';

    // Set enableTemplates to false to remove the button allowing users to save a pad as a template
    // and remove the template category in CryptDrive
    config.enableTemplates = true;

    // Set enableHistory to false to remove the "History" button in all the apps.
    config.enableHistory = true;

    /*  user passwords are hashed with scrypt, and salted with their username.
        this value will be appended to the username, causing the resulting hash
        to differ from other CryptPad instances if customized. This makes it
        such that anyone who wants to bruteforce common credentials must do so
        again on each CryptPad instance that they wish to attack.

        WARNING: this should only be set when your CryptPad instance is first
        created. Changing it at a later time will break logins for all existing
        users.
    */
    config.loginSalt = '';
    config.minimumPasswordLength = 8;

    // Amount of time (ms) before aborting the session when the algorithm cannot synchronize the pad
    config.badStateTimeout = 30000;

    // Customize the icon used for each application.
    // You can update the colors by making a copy of /customize.dist/src/less2/include/colortheme.less
    config.applicationsIcon = {
        file: 'cptools-file',
        fileupload: 'cptools-file-upload',
        folderupload: 'cptools-folder-upload',
        pad: 'cptools-richtext',
        code: 'cptools-code',
        slide: 'cptools-slide',
        poll: 'cptools-poll',
        whiteboard: 'cptools-whiteboard',
        todo: 'cptools-todo',
        contacts: 'fa-address-book',
        kanban: 'cptools-kanban',
        doc: 'fa-file-word-o',
        presentation: 'fa-file-powerpoint-o',
        sheet: 'cptools-sheet',
        drive: 'fa-hdd-o',
        teams: 'fa-users',
    };

    // Ability to create owned pads and expiring pads through a new pad creation screen.
    // The new screen can be disabled by the users in their settings page
    config.displayCreationScreen = true;

    // Prevent anonymous users from storing pads in their drive
    config.disableAnonymousStore = false;

    // Hide the usage bar in settings and drive
    //config.hideUsageBar = true;

    // Disable feedback for all the users and hide the settings part about feedback
    //config.disableFeedback = true;

    // Add new options in the share modal (extend an existing tab or add a new tab).
    // More info about how to use it on the wiki:
    // https://github.com/xwiki-labs/cryptpad/wiki/Application-config#configcustomizeshareoptions
    //config.customizeShareOptions = function (hashes, tabs, config) {};

    // Add code to be executed on every page before loading the user object. `isLoggedIn` (bool) is
    // indicating if the user is registered or anonymous. Here you can change the way anonymous users
    // work in CryptPad, use an external SSO or even force registration
    // *NOTE*: You have to call the `callback` function to continue the loading process
    //config.beforeLogin = function(isLoggedIn, callback) {};

    // Add code to be executed on every page after the user object is loaded (also work for
    // unregistered users). This allows you to interact with your users' drive
    // *NOTE*: You have to call the `callback` function to continue the loading process
    //config.afterLogin = function(api, callback) {};

    // Disabling the profile app allows you to import the profile informations (display name, avatar)
    // from an external source and make sure the users can't change them from CryptPad.
    // You can use config.afterLogin to import these values in the users' drive.
    //config.disableProfile = true;

    // Disable the use of webworkers and sharedworkers in CryptPad.
    // Workers allow us to run the websockets connection and open the user drive in a separate thread.
    // SharedWorkers allow us to load only one websocket and one user drive for all the browser tabs,
    // making it much faster to open new tabs.
    config.disableWorkers = false;

    // Teams are always loaded during the initial loading screen (for the first tab only if
    // SharedWorkers are available). Allowing users to be members of multiple teams can
    // make them have a very slow loading time. To avoid impacting the user experience
    // significantly, we're limiting the number of teams per user to 3 by default.
    // You can change this value here.
    //config.maxTeamsSlots = 5;

    // Each team is considered as a registered user by the server. Users and teams are indistinguishable
    // in the database so teams will offer the same storage limits as users by default.
    // It means that each team created by a user can increase their storage limit by +100%.
    // We're limiting the number of teams each user is able to own to 1 in order to make sure
    // users don't use "fake" teams (1 member) just to increase their storage limit.
    // You can change the value here.
    // config.maxOwnedTeams = 5;

    // The userlist displayed in collaborative documents is stored alongside the document data.
    // Everytime someone with edit rights joins a document or modify their user data (display
    // name, avatar, color, etc.), they update the "userlist" part of the document. When too many
    // editors are in the same document, all these changes increase the risks of conflicts which
    // require CPU time to solve. A "degraded" mode can now be set when a certain number of editors
    // are in a document at the same time. This mode disables the userlist, the chat and the
    // position of other users' cursor. You can configure the number of user from which the session
    // will enter into degraded mode. A big number may result in collaborative edition being broken,
    // but this number depends on the network and CPU performances of each user's device.
    config.degradedLimit = 8;

    // In "legacy" mode, one-time users were always creating an "anonymous" drive when visiting CryptPad
    // in which they could store their pads. The new "driveless" mode allow users to open an existing
    // pad without creating a drive in the background. The drive will only be created if they visit
    // a different page (Drive, Settings, etc.) or try to create a new pad themselves. You can disable
    // the driveless mode by changing the following value to "false"
    config.allowDrivelessMode = true;

    return config;
});
<|endoftext|>


<commit_before>sys = {
    __getattr__ : function(attr){
        if(attr==="stdout"){return $stdout}
        if(attr==="stderr"){return $stderr}
        else if(attr in this){return this[attr]}
        else{$raise('AttributeError','module sys has no attribute '+attr)}},
    __setattr__ : function(attr,value){
        if(attr==="stdout"){$stdout=value}
        if(attr==="stderr"){$stderr=value}
    },
    has_local_storage:typeof(Storage)!=="undefined",
    version_info:[1,0,"20121225-174458"]
}<commit_msg>sys.js : replace $stdout by document.$stdout<commit_after>sys = {
    __getattr__ : function(attr){
        if(attr==="stdout"){return document.$stdout}
        if(attr==="stderr"){return document.$stderr}
        else if(attr in this){return this[attr]}
        else{$raise('AttributeError','module sys has no attribute '+attr)}},
    __setattr__ : function(attr,value){
        if(attr==="stdout"){document.$stdout=value}
        if(attr==="stderr"){document.$stderr=value}
    },
    has_local_storage:typeof(Storage)!=="undefined",
    version_info:[1,0,"20121228-085057"]
}<|endoftext|>


<commit_before>// {{{ function SwatSimpleColorEntry

/**
 * Simple color entry widget
 *
 * @param string id
 * @param Array colors
 * @param string none_option_title
 *
 * @copyright 2005-2010 silverorange Inc.
 */

function SwatSimpleColorEntry(id, colors, none_option_title)
{
	SwatSimpleColorEntry.superclass.constructor.call(this, id);

	this.colors = colors;
	this.none_option_title = none_option_title;
	this.input_tag = document.getElementById(this.id + '_value');

	this.hex_input_tag = document.createElement('input');
	this.hex_input_tag.type = 'text';
	this.hex_input_tag.id = this.id + '_hex_color';
	this.hex_input_tag.size = 6;

	YAHOO.util.Event.on(this.hex_input_tag, 'change',
		this.handleInputChange, this, true);

	YAHOO.util.Event.on(this.hex_input_tag, 'keyup',
		this.handleInputChange, this, true);

	// this tries to make a square palette
	this.columns = Math.ceil(Math.sqrt(this.colors.length));

	this.current_color = null;
	this.colorChangeEvent = new YAHOO.util.CustomEvent('colorChange');

	this.setColor(this.input_tag.value);
}

// }}}
// {{{ YAHOO.lang.extend(SwatSimpleColorEntry, SwatAbstractOverlay)

YAHOO.lang.extend(SwatSimpleColorEntry, SwatAbstractOverlay, {
// {{{ getContent: function()

getContent: function()
{
	var table = document.createElement('table');
	table.className = 'swat-simple-color-entry-table';
	table.cellSpacing = '1';

	var tbody = document.createElement('tbody');

	if (this.colors.length % this.columns == 0)
		var num_cells = this.colors.length
	else
		var num_cells = this.colors.length +
			(this.columns - (this.colors.length % this.columns));

	var trow;
	var tcell;
	var anchor;
	var text;

	if (this.none_option_title !== null) {
		trow = document.createElement('tr');
		tcell = document.createElement('td');
		tcell.id = this.id + '_palette_null';
		tcell.colSpan = this.columns;
		YAHOO.util.Dom.addClass(tcell,
			'swat-simple-color-entry-palette-blank');

		text = document.createTextNode(this.none_option_title);

		anchor = document.createElement('a');
		anchor.href = '#';
		anchor.appendChild(text);
		tcell.appendChild(anchor);
		trow.appendChild(tcell);
		tbody.appendChild(trow);

		YAHOO.util.Event.addListener(anchor, 'click', this.selectNull,
			this, true);
	}

	for (var i = 0; i < num_cells; i++) {
		if (i % this.columns == 0)
			trow = document.createElement('tr');

		tcell = document.createElement('td');
		text = document.createTextNode(' '); // non-breaking UTF-8 space

		if (i < this.colors.length) {
			tcell.id = this.id + '_palette_' + i;
			tcell.style.background = '#' + this.colors[i];

			anchor = document.createElement('a');
			anchor.href = '#';
			anchor.appendChild(text);

			YAHOO.util.Event.addListener(anchor, 'click', this.selectColor,
				this, true);

			tcell.appendChild(anchor);
		} else {
			YAHOO.util.Dom.addClass(tcell,
				'swat-simple-color-entry-palette-blank');

			tcell.appendChild(text);
		}

		trow.appendChild(tcell);

		if ((i + 1) % this.columns == 0)
			tbody.appendChild(trow);
	}

	table.appendChild(tbody);

	var hex_div = document.createElement('div');
	hex_div.className = 'swat-simple-color-entry-palette-hex-color';

	var label_tag = document.createElement('label');
	label_tag.htmlFor = this.id + '_hex_color';
	var title = document.createTextNode('#');
	label_tag.appendChild(title);

	hex_div.appendChild(label_tag);
	hex_div.appendChild(this.hex_input_tag);

	var div_tag = document.createElement('div');
	div_tag.appendChild(table);
	div_tag.appendChild(hex_div);

	return div_tag;
}

// }}}
});

// }}}
// {{{ SwatSimpleColorEntry.prototype.handleInputChange

SwatSimpleColorEntry.prototype.handleInputChange = function()
{
	var color = this.hex_input_tag.value;

	if (color[0] == '#') {
		color = color.slice(1, color.length - 1);
	}

	if (color.length == 3) {
		var hex3 = /^[0-9a-f]{3}$/i;
		if (!hex3.test(color)) {
			color = null;
		}
	} else if (color.length == 6) {
		var hex6 = /^[0-9a-f]{6}$/i;
		if (!hex6.test(color)) {
			color = null;
		}
	} else {
		color = null;
	}

	this.setColor(color);
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.setColor

/**
 * Sets the value of the color entry input tag to the selected color and
 * highlights the selected color
 *
 * @param number color the hex value of the color
 */
SwatSimpleColorEntry.prototype.setColor = function(color)
{
	var changed = (this.current_color != color);

	if (changed) {
		this.input_tag.value = color;

		if (color === null) {
			YAHOO.util.Dom.setStyle(this.toggle_button_content,
				'background', '');
		} else {
			if (this.hex_input_tag.value != color) {
				this.hex_input_tag.value = color;
			}
			YAHOO.util.Dom.setStyle(this.toggle_button_content,
				'background', '#' + color);
		}

		this.current_color = color;

		this.value_field.value = color;

		if (color === null) {
			this.colorChangeEvent.fire(null);
		} else {
			this.colorChangeEvent.fire('#' + color);
		}

		this.highlightPalleteEntry(color);
	}
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.selectNull

/**
 * Event handler that sets the color to null
 *
 * @param Event the event that triggered this select.
 */
SwatSimpleColorEntry.prototype.selectNull = function(event)
{
	this.hex_input_tag.value = '';
	YAHOO.util.Event.preventDefault(event);
	this.setColor(null);
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.selectColor

/**
 * Event handler that sets the color to the selected color
 *
 * @param Event the event that triggered this select.
 */
SwatSimpleColorEntry.prototype.selectColor = function(event)
{
	YAHOO.util.Event.preventDefault(event);
	var cell = YAHOO.util.Event.getTarget(event);
	var color_index = cell.parentNode.id.split('_palette_')[1];

	this.setColor(this.colors[color_index]);
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.highlightPalleteEntry

/**
 * Highlights a pallete entry
 *
 * @param number color the hex value of the color
 */
SwatSimpleColorEntry.prototype.highlightPalleteEntry = function(color)
{
	if (this.none_option_title !== null) {
		var null_entry = document.getElementById(this.id + '_palette_null');

		if (color === null) {
			YAHOO.util.Dom.addClass(null_entry,
				'swat-simple-color-entry-palette-selected');
		} else {
			YAHOO.util.Dom.removeClass(null_entry,
				'swat-simple-color-entry-palette-selected');
		}
	}

	for (var i = 0; i < this.colors.length; i++) {
		var palette_entry =
			document.getElementById(this.id + '_palette_' + i);

		if (this.current_color !== null &&
			this.colors[i].toLowerCase() ==
			this.current_color.toLowerCase()) {

			YAHOO.util.Dom.addClass(palette_entry,
				'swat-simple-color-entry-palette-selected');
		} else {
			YAHOO.util.Dom.removeClass(palette_entry,
				'swat-simple-color-entry-palette-selected');
		}
	}
}

// }}}
<commit_msg>Handle invalid colors better.<commit_after>// {{{ function SwatSimpleColorEntry

/**
 * Simple color entry widget
 *
 * @param string id
 * @param Array colors
 * @param string none_option_title
 *
 * @copyright 2005-2010 silverorange Inc.
 */

function SwatSimpleColorEntry(id, colors, none_option_title)
{
	SwatSimpleColorEntry.superclass.constructor.call(this, id);

	this.colors = colors;
	this.none_option_title = none_option_title;
	this.input_tag = document.getElementById(this.id + '_value');

	this.hex_input_tag = document.createElement('input');
	this.hex_input_tag.type = 'text';
	this.hex_input_tag.id = this.id + '_hex_color';
	this.hex_input_tag.size = 6;

	YAHOO.util.Event.on(this.hex_input_tag, 'change',
		this.handleInputChange, this, true);

	YAHOO.util.Event.on(this.hex_input_tag, 'keyup',
		this.handleInputChange, this, true);

	// this tries to make a square palette
	this.columns = Math.ceil(Math.sqrt(this.colors.length));

	this.current_color = null;
	this.colorChangeEvent = new YAHOO.util.CustomEvent('colorChange');

	this.setColor(this.input_tag.value);
}

// }}}
// {{{ YAHOO.lang.extend(SwatSimpleColorEntry, SwatAbstractOverlay)

YAHOO.lang.extend(SwatSimpleColorEntry, SwatAbstractOverlay, {
// {{{ getContent: function()

getContent: function()
{
	var table = document.createElement('table');
	table.className = 'swat-simple-color-entry-table';
	table.cellSpacing = '1';

	var tbody = document.createElement('tbody');

	if (this.colors.length % this.columns == 0)
		var num_cells = this.colors.length
	else
		var num_cells = this.colors.length +
			(this.columns - (this.colors.length % this.columns));

	var trow;
	var tcell;
	var anchor;
	var text;

	if (this.none_option_title !== null) {
		trow = document.createElement('tr');
		tcell = document.createElement('td');
		tcell.id = this.id + '_palette_null';
		tcell.colSpan = this.columns;
		YAHOO.util.Dom.addClass(tcell,
			'swat-simple-color-entry-palette-blank');

		text = document.createTextNode(this.none_option_title);

		anchor = document.createElement('a');
		anchor.href = '#';
		anchor.appendChild(text);
		tcell.appendChild(anchor);
		trow.appendChild(tcell);
		tbody.appendChild(trow);

		YAHOO.util.Event.addListener(anchor, 'click', this.selectNull,
			this, true);
	}

	for (var i = 0; i < num_cells; i++) {
		if (i % this.columns == 0)
			trow = document.createElement('tr');

		tcell = document.createElement('td');
		text = document.createTextNode(' '); // non-breaking UTF-8 space

		if (i < this.colors.length) {
			tcell.id = this.id + '_palette_' + i;
			tcell.style.background = '#' + this.colors[i];

			anchor = document.createElement('a');
			anchor.href = '#';
			anchor.appendChild(text);

			YAHOO.util.Event.addListener(anchor, 'click', this.selectColor,
				this, true);

			tcell.appendChild(anchor);
		} else {
			YAHOO.util.Dom.addClass(tcell,
				'swat-simple-color-entry-palette-blank');

			tcell.appendChild(text);
		}

		trow.appendChild(tcell);

		if ((i + 1) % this.columns == 0)
			tbody.appendChild(trow);
	}

	table.appendChild(tbody);

	var hex_div = document.createElement('div');
	hex_div.className = 'swat-simple-color-entry-palette-hex-color';

	var label_tag = document.createElement('label');
	label_tag.htmlFor = this.id + '_hex_color';
	var title = document.createTextNode('#');
	label_tag.appendChild(title);

	hex_div.appendChild(label_tag);
	hex_div.appendChild(this.hex_input_tag);

	var div_tag = document.createElement('div');
	div_tag.appendChild(table);
	div_tag.appendChild(hex_div);

	return div_tag;
}

// }}}
});

// }}}
// {{{ SwatSimpleColorEntry.prototype.handleInputChange

SwatSimpleColorEntry.prototype.handleInputChange = function()
{
	var color = this.hex_input_tag.value;

	if (color[0] == '#') {
		color = color.slice(1, color.length - 1);
	}

	if (color.length == 3) {
		var hex3 = /^[0-9a-f]{3}$/i;
		if (!hex3.test(color)) {
			color = null;
		}
	} else if (color.length == 6) {
		var hex6 = /^[0-9a-f]{6}$/i;
		if (!hex6.test(color)) {
			color = null;
		}
	} else {
		color = null;
	}

	this.setColor(color);
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.setColor

/**
 * Sets the value of the color entry input tag to the selected color and
 * highlights the selected color
 *
 * @param number color the hex value of the color
 */
SwatSimpleColorEntry.prototype.setColor = function(color)
{
	if (!/^([0-9a-f]{3}){1,2}$/i.test(color)) {
		color = null;
	}

	var changed = (this.current_color != color);

	if (changed) {
		this.input_tag.value = color;

		if (color === null) {
			YAHOO.util.Dom.setStyle(this.toggle_button_content,
				'background', null);
		} else {
			if (this.hex_input_tag.value != color) {
				this.hex_input_tag.value = color;
			}
			YAHOO.util.Dom.setStyle(this.toggle_button_content,
				'background', '#' + color);
		}

		this.current_color = color;

		this.value_field.value = color;

		if (color === null) {
			this.colorChangeEvent.fire(null);
		} else {
			this.colorChangeEvent.fire('#' + color);
		}

		this.highlightPalleteEntry(color);
	}
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.selectNull

/**
 * Event handler that sets the color to null
 *
 * @param Event the event that triggered this select.
 */
SwatSimpleColorEntry.prototype.selectNull = function(event)
{
	this.hex_input_tag.value = '';
	YAHOO.util.Event.preventDefault(event);
	this.setColor(null);
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.selectColor

/**
 * Event handler that sets the color to the selected color
 *
 * @param Event the event that triggered this select.
 */
SwatSimpleColorEntry.prototype.selectColor = function(event)
{
	YAHOO.util.Event.preventDefault(event);
	var cell = YAHOO.util.Event.getTarget(event);
	var color_index = cell.parentNode.id.split('_palette_')[1];

	this.setColor(this.colors[color_index]);
}

// }}}
// {{{ SwatSimpleColorEntry.prototype.highlightPalleteEntry

/**
 * Highlights a pallete entry
 *
 * @param number color the hex value of the color
 */
SwatSimpleColorEntry.prototype.highlightPalleteEntry = function(color)
{
	if (this.none_option_title !== null) {
		var null_entry = document.getElementById(this.id + '_palette_null');

		if (color === null) {
			YAHOO.util.Dom.addClass(null_entry,
				'swat-simple-color-entry-palette-selected');
		} else {
			YAHOO.util.Dom.removeClass(null_entry,
				'swat-simple-color-entry-palette-selected');
		}
	}

	for (var i = 0; i < this.colors.length; i++) {
		var palette_entry =
			document.getElementById(this.id + '_palette_' + i);

		if (this.current_color !== null &&
			this.colors[i].toLowerCase() ==
			this.current_color.toLowerCase()) {

			YAHOO.util.Dom.addClass(palette_entry,
				'swat-simple-color-entry-palette-selected');
		} else {
			YAHOO.util.Dom.removeClass(palette_entry,
				'swat-simple-color-entry-palette-selected');
		}
	}
}

// }}}
<|endoftext|>


<commit_before>angular.module('pvta.controllers').controller('StopMapController', function($scope, $ionicLoading, Map, LatLong){
  var bounds = new google.maps.LatLngBounds();
  var directionsDisplay;
  var directionsService = new google.maps.DirectionsService();

  var mapOptions = {
    //sets the center to Haigis Mall
    //This may have to change if we end up deploying this to
    //the entire PVTA ridership
    center: new google.maps.LatLng(42.386270, -72.525844),
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  };

  $scope.map = new google.maps.Map(document.getElementById("map"), mapOptions);
  Map.init($scope.map, bounds);

  function placeStop(){
    var stopLocation = _.first(LatLong.getAll());
    var loc = new google.maps.LatLng(stopLocation.lat, stopLocation.long);
    Map.addMapListener(Map.placeDesiredMarker(loc), 'Here is your stop!');
    return loc;
  }

  function calculateDirections(){
    var cb = function(position){
      start = position
      var end = placeStop();
      var request = {
        origin: start,
        destination: end,
        travelMode: google.maps.TravelMode.WALKING
      };
      directionsService.route(request, function(result, status){
        if(status == google.maps.DirectionsStatus.OK){
          directionsDisplay.setDirections(result);
        }
        $ionicLoading.hide();
      });
    }
    Map.plotCurrentLocation(cb);
  }

  $scope.$on('$ionicView.enter', function () {
    $ionicLoading.show({});
    directionsDisplay = new google.maps.DirectionsRenderer();
    directionsDisplay.setMap($scope.map);
    calculateDirections();
    //directionsDisplay.setPanel(document.getElementById("right-panel"));
  });

})
<commit_msg>attempted to fix a codeclimate issue<commit_after>angular.module('pvta.controllers').controller('StopMapController', function($scope, $ionicLoading, Map, LatLong){
  var bounds = new google.maps.LatLngBounds();
  var directionsDisplay;
  var directionsService = new google.maps.DirectionsService();

  var mapOptions = {
    //sets the center to Haigis Mall
    //This may have to change if we end up deploying this to
    //the entire PVTA ridership
    center: new google.maps.LatLng(42.386270, -72.525844),
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  };

  $scope.map = new google.maps.Map(document.getElementById("map"), mapOptions);
  Map.init($scope.map, bounds);

  function placeStop(){
    var stopLocation = _.first(LatLong.getAll());
    var loc = new google.maps.LatLng(stopLocation.lat, stopLocation.long);
    Map.addMapListener(Map.placeDesiredMarker(loc), 'Here is your stop!');
    return loc;
  }

  function calculateDirections(){
    var cb = function(position){
      start = position
      var end = placeStop();
      var request = {
        origin: start,
        destination: end,
        travelMode: google.maps.TravelMode.WALKING
      };
      directionsService.route(request, function(result, status){
        if(status === google.maps.DirectionsStatus.OK){
          directionsDisplay.setDirections(result);
        }
        $ionicLoading.hide();
      });
    }
    Map.plotCurrentLocation(cb);
  }

  $scope.$on('$ionicView.enter', function () {
    $ionicLoading.show({});
    directionsDisplay = new google.maps.DirectionsRenderer();
    directionsDisplay.setMap($scope.map);
    directionsDisplay.setPanel(document.getElementById("right-panel"));
    calculateDirections();
  });

})
<|endoftext|>


<commit_before>var expect = require('chai').expect;
var request = require('supertest');
var bodyParser = require('body-parser');

var conf = require('../../../app/config');
var fixtures = require('../../fixtures/fixtures');

var apiSuffix = conf.get('apiSuffix') + "subscriptions";
console.log(apiSuffix);
var authHeader = {'Authorization': 'Bearer ' + conf.get('apiKey')}

var app = require('../../../app');
app.use(bodyParser.urlencoded({
  extended: true
}));

// @TODO: Tests for 404s. Tests for bad data

describe('Subscription routes', function() {
  describe('show subscriptions', function() {
    it('should show all subscriptions', function(done) {
      request(app)
        .get(apiSuffix)
        .set(authHeader)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(200);
          expect(res.body).to.be.an('array');
          expect(res.body.length).to.equal(3);
          expect(res.body[0].shopName).to.equal('UnicornFluff');
          done();
        });
    });
  });

  describe('show single subscription', function() {
    it('should show a single subscription', function(done) {
      var fixtureId = fixtures.Subscription.sub3._id;
      request(app)
        .get(apiSuffix + '/' + fixtureId)
        .set(authHeader)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(200);
          expect(res.body).to.be.an('object');
          expect(res.body.shopName).to.equal('CoolKidzSkateboardz');
          done();
        });
    });
  });

  describe('create subscription', function() {
    it('should create a subscription', function(done) {

      var subscription = {
        shopName: "Artisanal Artisans",
        frequency: 30,
        userId: "cantbebotheredgeneratingamongodbuserid"
      };

      request(app)
        .post(apiSuffix)
        .set(authHeader)
        .type('form')
        .send(subscription)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(201);
          expect(res.get('Location')).to.exist;
          expect(res.body).to.be.empty;
          done();
        });
    });
  });

  describe('update subscription', function() {
    it('should update an existing subscription', function(done) {

      var fixtureId = fixtures.Subscription.sub3._id;

      var subscription = {
        frequency: 120,
        shopName: 'Artisanal Goat Herding Supplies'
      };

      request(app)
        .put(apiSuffix + '/' + fixtureId)
        .set(authHeader)
        .type('form')
        .send(subscription)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(200);
          expect(res.body.frequency).to.equal(120);
          expect(res.body.shopName).to.equal('Artisanal Goat Herding Supplies');
          done();
        });
    });
  });

  describe('delete subscription', function() {
    it('should delete an existing subscription', function(done) {

      var fixtureId = fixtures.Subscription.sub3._id;

      request(app)
        .delete(apiSuffix + '/' + fixtureId)
        .set(authHeader)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(204);
          expect(res.body).to.be.empty;
          done();
        });
    });
  });
});
<commit_msg>removed  console.log<commit_after>var expect = require('chai').expect;
var request = require('supertest');
var bodyParser = require('body-parser');

var conf = require('../../../app/config');
var fixtures = require('../../fixtures/fixtures');

var apiSuffix = conf.get('apiSuffix') + "subscriptions";
var authHeader = {'Authorization': 'Bearer ' + conf.get('apiKey')}

var app = require('../../../app');
app.use(bodyParser.urlencoded({
  extended: true
}));

// @TODO: Tests for 404s. Tests for bad data

describe('Subscription routes', function() {
  describe('show subscriptions', function() {
    it('should show all subscriptions', function(done) {
      request(app)
        .get(apiSuffix)
        .set(authHeader)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(200);
          expect(res.body).to.be.an('array');
          expect(res.body.length).to.equal(3);
          expect(res.body[0].shopName).to.equal('UnicornFluff');
          done();
        });
    });
  });

  describe('show single subscription', function() {
    it('should show a single subscription', function(done) {
      var fixtureId = fixtures.Subscription.sub3._id;
      request(app)
        .get(apiSuffix + '/' + fixtureId)
        .set(authHeader)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(200);
          expect(res.body).to.be.an('object');
          expect(res.body.shopName).to.equal('CoolKidzSkateboardz');
          done();
        });
    });
  });

  describe('create subscription', function() {
    it('should create a subscription', function(done) {

      var subscription = {
        shopName: "Artisanal Artisans",
        frequency: 30,
        userId: "cantbebotheredgeneratingamongodbuserid"
      };

      request(app)
        .post(apiSuffix)
        .set(authHeader)
        .type('form')
        .send(subscription)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(201);
          expect(res.get('Location')).to.exist;
          expect(res.body).to.be.empty;
          done();
        });
    });
  });

  describe('update subscription', function() {
    it('should update an existing subscription', function(done) {

      var fixtureId = fixtures.Subscription.sub3._id;

      var subscription = {
        frequency: 120,
        shopName: 'Artisanal Goat Herding Supplies'
      };

      request(app)
        .put(apiSuffix + '/' + fixtureId)
        .set(authHeader)
        .type('form')
        .send(subscription)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(200);
          expect(res.body.frequency).to.equal(120);
          expect(res.body.shopName).to.equal('Artisanal Goat Herding Supplies');
          done();
        });
    });
  });

  describe('delete subscription', function() {
    it('should delete an existing subscription', function(done) {

      var fixtureId = fixtures.Subscription.sub3._id;

      request(app)
        .delete(apiSuffix + '/' + fixtureId)
        .set(authHeader)
        .end(function(err, res) {
          expect(err).to.equal(null);
          expect(res.statusCode).to.equal(204);
          expect(res.body).to.be.empty;
          done();
        });
    });
  });
});
<|endoftext|>


<commit_before>function Plugins( $, pluginlist, functions, toolbar )
{
  if ( Plugins.prototype._instance )
  {
    return Plugins.prototype._instance;
  }
  Plugins.prototype._instance = this;

  var pub = {
    'PluginInfo' : PluginInfo,
    'register' : register,
    'serialize' : serialize,
    'init' : parseHash,
    'exec' : executeByName
  };

  var FORMAT = '0';
  var PLUGIN_END_MARKER = '_';

  var plugins = {};
  var loading = {};

  $( window ).hashchange( function()
  {
    parseHash();
  } );

  function register( pluginInfo )
  {
    plugins[pluginInfo.name] = pluginInfo;
  }

  function list()
  {
    return plugins.slice( 0 );
  }

  function PluginInfo( info )
  {
    this.name = info.name;
    this.instance = info.instance;
    this.render = info.render;
    this.serialize = info.serialize;
  }

  function executeByName( name, func )
  {
    if ( plugins[name] )
    {
      func( plugins[name].instance, plugins[name], pub );
    }
    else
    {
      if ( typeof loading[name] !== 'undefined' )
      {
        // Store function to execute after load.
        loading[name].push( func );
      }
      else
      {
        loading[name] = [];
        require( [ 'plugins/' + name, 'plugins' ], function( dontUse, pluginsModule )
        {
          var instance = plugins[name].instance;
          // console.log( 'fresh loaded ', pluginsModule );
          func( instance, plugins[name], pluginsModule );
          for ( var i = 0, len = loading[name].length; i < len; i++ )
          {
            loading[name][i]( instance, plugins[name], pluginsModule );
          }
          loading[name] = undefined;
        } );
      }
    }
  }

  function render( name, format, data )
  {
    executeByName( name, function( instance )
    {
      if ( info.render === true )
      {
        instance.render( format, data );
      }
    } );
  }

  function setData( id, format, data )
  {
    executeByName( pluginlist.idToName( id ), function( instance )
    {
      instance.setData( format, data );
    } );
  }

  function setDataAndConfigure( input )
  {
    executeByName( input.name, function( instance, info, pluginsModule )
    {
      instance.setData( input.format, input.data );
      if ( 'postRender' in info )
      {
        pluginsModule.executeByName( info.postRender.module, function( moduleInstance )
        {
          moduleInstance.addPostRenderer( info.postRender.func );
        } );
      }
    } );
  }

  function setDataAndRender( input )
  {
    executeByName( input.name, function( instance, info, pluginsModule )
    {
      instance.setData( input.format, input.data );
      if ( info.render === true )
      {
        instance.render();
      }
    } );
  }

  function PluginInput( pluginId, pluginFormat, pluginData )
  {
    this.id = pluginId;
    this.format = pluginFormat;
    this.data = pluginData;
    this.name = pluginlist.idToName( pluginId );
  }

  function parseHash()
  {
    var PLUGIN_END_MARKER = '_';
    var hash = window.location.hash;
    var readMode = false;
    if ( hash.length > 0 )
    {
      var topLevelFormat = hash.charAt( 2 );
      if ( topLevelFormat !== '0' )
      {
        // we'll handle this better when we're actually at format
        // version 1.
        throw 'Unknown URL format.';
      }
      var pluginSections = decodeURIComponent( hash.substring( 3 ) ).split( PLUGIN_END_MARKER );
      var plugins = [];
      for ( var i = 0; i < pluginSections.length; i++ )
      {
        var plugin = pluginSections[i];
        var pluginId = functions.getNumber( plugin.substr( 0, 2 ) );
        var pluginFormat = functions.getNumber( plugin.substr( 2, 1 ) );
        var pluginData = plugin.substr( 3 );
        var pluginInput = new PluginInput( pluginId, pluginFormat, pluginData );
        plugins.push( pluginInput );
        setDataAndRender( pluginInput );
      }
      readMode = true;
    }
    else
    {
      toolbar.setEditMode( true );
      $( '#help' ).modal();
    }
    if ( readMode )
    {
      toolbar.hideOrShow( 'hide' );
    }
  }

  function serialize()
  {
    var data = [];
    $.each( plugins, function( name, pluginInfo )
    {
      if ( pluginInfo.serialize )
      {
        var serialized = pluginInfo.instance.serialize();
        if ( serialized )
        {
          data.push( serialized );
        }
      }
    } );
    data.sort( compare );
    var result = '!' + FORMAT + data.join( PLUGIN_END_MARKER );
    return result.length < 3 ? '' : result;
  }

  function compare( a, b )
  {
    var aNum = functions.getNumber( a.substr( 0, 2 ) );
    var bNum = functions.getNumber( b.substr( 0, 2 ) );
    return aNum - bNum;
  }

  return pub;
}

define( 'plugins', [ 'jquery', 'pluginlist', 'functions', 'toolbar' ], function( $, pluginlist, functions, toolbar )
{
  return new Plugins( $, pluginlist, functions, toolbar );
} );
<commit_msg>Makes sure to perform plugin executions in the correct order.<commit_after>function Plugins( $, pluginlist, functions, toolbar )
{
  if ( Plugins.prototype._instance )
  {
    return Plugins.prototype._instance;
  }
  Plugins.prototype._instance = this;

  var pub = {
    'PluginInfo' : PluginInfo,
    'register' : register,
    'serialize' : serialize,
    'init' : parseHash,
    'exec' : executeByName
  };

  var FORMAT = '0';
  var PLUGIN_END_MARKER = '_';

  var plugins = {};
  var loading = {};

  $( window ).hashchange( function()
  {
    parseHash();
  } );

  function register( pluginInfo )
  {
    plugins[pluginInfo.name] = pluginInfo;
  }

  function list()
  {
    return plugins.slice( 0 );
  }

  function PluginInfo( info )
  {
    this.name = info.name;
    this.instance = info.instance;
    this.render = info.render;
    this.serialize = info.serialize;
  }

  function executeByName( name, func )
  {
    if ( typeof loading[name] !== 'undefined' )
    {
      // Store function to execute after load.
      loading[name].push( func );
    }
    else if ( plugins[name] )
    {
      // The script has been loaded and batched executions run.
      func( plugins[name].instance, plugins[name], pub );
    }
    else
    {
      // Start loading the script.
      loading[name] = [];
      require( [ 'plugins/' + name, 'plugins' ], function( dontUse, pluginsModule )
      {
        var instance = plugins[name].instance;
        func( instance, plugins[name], pluginsModule );
        for ( var i = 0, len = loading[name].length; i < len; i++ )
        {
          loading[name][i]( instance, plugins[name], pluginsModule );
        }
        delete loading[name];
      } );
    }
  }

  function render( name, format, data )
  {
    executeByName( name, function( instance )
    {
      if ( info.render === true )
      {
        instance.render( format, data );
      }
    } );
  }

  function setData( id, format, data )
  {
    executeByName( pluginlist.idToName( id ), function( instance )
    {
      instance.setData( format, data );
    } );
  }

  function setDataAndConfigure( input )
  {
    executeByName( input.name, function( instance, info, pluginsModule )
    {
      instance.setData( input.format, input.data );
      if ( 'postRender' in info )
      {
        pluginsModule.executeByName( info.postRender.module, function( moduleInstance )
        {
          moduleInstance.addPostRenderer( info.postRender.func );
        } );
      }
    } );
  }

  function setDataAndRender( input )
  {
    executeByName( input.name, function( instance, info, pluginsModule )
    {
      instance.setData( input.format, input.data );
      if ( info.render === true )
      {
        instance.render();
      }
    } );
  }

  function PluginInput( pluginId, pluginFormat, pluginData )
  {
    this.id = pluginId;
    this.format = pluginFormat;
    this.data = pluginData;
    this.name = pluginlist.idToName( pluginId );
  }

  function parseHash()
  {
    var PLUGIN_END_MARKER = '_';
    var hash = window.location.hash;
    var readMode = false;
    if ( hash.length > 0 )
    {
      var topLevelFormat = hash.charAt( 2 );
      if ( topLevelFormat !== '0' )
      {
        // we'll handle this better when we're actually at format
        // version 1.
        throw 'Unknown URL format.';
      }
      var pluginSections = decodeURIComponent( hash.substring( 3 ) ).split( PLUGIN_END_MARKER );
      var plugins = [];
      for ( var i = 0; i < pluginSections.length; i++ )
      {
        var plugin = pluginSections[i];
        var pluginId = functions.getNumber( plugin.substr( 0, 2 ) );
        var pluginFormat = functions.getNumber( plugin.substr( 2, 1 ) );
        var pluginData = plugin.substr( 3 );
        var pluginInput = new PluginInput( pluginId, pluginFormat, pluginData );
        plugins.push( pluginInput );
        setDataAndRender( pluginInput );
      }
      readMode = true;
    }
    else
    {
      toolbar.setEditMode( true );
      $( '#help' ).modal();
    }
    if ( readMode )
    {
      toolbar.hideOrShow( 'hide' );
    }
  }

  function serialize()
  {
    var data = [];
    $.each( plugins, function( name, pluginInfo )
    {
      if ( pluginInfo.serialize )
      {
        var serialized = pluginInfo.instance.serialize();
        if ( serialized )
        {
          data.push( serialized );
        }
      }
    } );
    data.sort( compare );
    var result = '!' + FORMAT + data.join( PLUGIN_END_MARKER );
    return result.length < 3 ? '' : result;
  }

  function compare( a, b )
  {
    var aNum = functions.getNumber( a.substr( 0, 2 ) );
    var bNum = functions.getNumber( b.substr( 0, 2 ) );
    return aNum - bNum;
  }

  return pub;
}

define( 'plugins', [ 'jquery', 'pluginlist', 'functions', 'toolbar' ], function( $, pluginlist, functions, toolbar )
{
  return new Plugins( $, pluginlist, functions, toolbar );
} );
<|endoftext|>


<commit_before>import { choose, id } from '../../fn/module.js';

const assign = Object.assign;

/**
config

```{
	headers:    fn(data),    // Must return an object with properties to add to the header
	body:       fn(data),    // Must return an object to send as data
	onresponse: function(response)
}```
*/

export const config = {
    // Takes data, returns headers
	headers: function(data) { return {}; },

	// Takes data (can be FormData object or plain object), returns data
	body: id,

	// Takes response, returns response
	onresponse: function(response) {
		// If redirected, navigate the browser away from here. Can get
		// annoying when receiving 404s, maybe not a good default...
		if (response.redirected) {
			window.location = response.url;
			return;
		}

		return response;
	}
};

const createHeaders = choose({
	'application/x-www-form-urlencoded': function(headers) {
		return assign(headers, {
			"Content-Type": 'application/x-www-form-urlencoded',
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'application/json': function(headers) {
		return assign(headers, {
			"Content-Type": "application/json; charset=utf-8",
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'multipart/form-data': function(headers) {
		return assign(headers, {
			"Content-Type": 'multipart/form-data',
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'audio/wav': function(headers) {
		return assign(headers, {
			"Content-Type": 'audio/wav',
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'default': function(headers) {
		return assign(headers, {
			"Content-Type": 'application/x-www-form-urlencoded',
			"X-Requested-With": "XMLHttpRequest"
		});
	}
});

const createBody = choose({
	'application/json': function(data) {
		return data.get ?
			formDataToJSON(data) :
			JSON.stringify(data);
	},

	'application/x-www-form-urlencoded': function(data) {
		return data.get ?
			formDataToQuery(data) :
			dataToQuery(data) ;
	},

	'multipart/form-data': function(data) {
		// Mmmmmhmmm?
		return data.get ?
            data :
            dataToFormData(data) ;
	}
});

function formDataToJSON(formData) {
	return JSON.stringify(
		// formData.entries() is an iterator, not an array
		Array
		.from(formData.entries())
		.reduce(function(output, entry) {
			output[entry[0]] = entry[1];
			return output;
		}, {})
	);
}

function formDataToQuery(data) {
	return new URLSearchParams(data).toString();
}

function dataToQuery(data) {
	return Object.keys(data).reduce((params, key) => {
		params.append(key, data[key]);
		return params;
	}, new URLSearchParams());
}

function dataToFormData(data) {
    throw new Error('TODO: dataToFormData(data)');
}

function urlFromData(url, data) {
	// Form data
	return data instanceof FormData ?
		url + '?' + formDataToQuery(data) :
		url + '?' + dataToQuery(data) ;
}

function createOptions(method, mimetype, data, controller) {
	return method === 'GET' ? {
		method:  method,
		headers: createHeaders(mimetype, config.headers ? config.headers(data) : {}),
		credentials: 'same-origin',
		signal: controller && controller.signal
	} : {
		method:  method,
		// Process headers before body, allowing us to read a CSRFToken,
        // which may be in data, in createHeaders() before removing it
        // from data in body().
		headers: createHeaders(mimetype, config.headers ? config.headers(data) : {}),
		body:    createBody(mimetype, config.body ? config.body(data) : data),
		credentials: 'same-origin',
		signal: controller && controller.signal
	} ;
}

const responders = {
	'text/html': respondText,
	'application/json': respondJSON,
	'multipart/form-data': respondForm,
	'application/x-www-form-urlencoded': respondForm,
	'audio': respondBlob,
	'audio/wav': respondBlob,
	'audio/m4a': respondBlob
};

function respondBlob(response) {
	return response.blob();
}

function respondJSON(response) {
	return response.json();
}

function respondForm(response) {
	return response.formData();
}

function respondText(response) {
	return response.text();
}

function respond(response) {
	if (config.onresponse) {
		response = config.onresponse(response);
	}

	if (!response.ok) {
		throw new Error(response.statusText + '');
	}

	// Get mimetype from Content-Type, remembering to hoik off any
	// parameters first
	const mimetype = response.headers
	.get('Content-Type')
	.replace(/\;.*$/, '');

	return responders[mimetype](response);
}

/**
request(type, mimetype, url, data)
*/

export default function request(type = 'GET', mimetype = 'application/json', url, data) {
	const method = type.toUpperCase();

	// If this is a GET and there is data, append data to the URL query string
	if (method === 'GET' && data) {
		url = urlFromData(url, data);
	}

	// param[4] is an optional abort controller
	return fetch(url, createOptions(method, mimetype, data, arguments[4]))
	.then(respond);
}

/**
requestGet(url)
A shortcut for `request('get', 'application/json', url)`
*/

export function requestGet(url) {
	return request('GET', 'application/json', url, {});
}

/**
requestPatch(url, data)
A shortcut for `request('patch', 'application/json', url, data)`
*/

export function requestPatch(url, data) {
	return request('PATCH', 'application/json', url, data);
}

/**
requestPost(url, data)
A shortcut for `request('post', 'application/json', url, data)`
*/

export function requestPost(url, data) {
	return request('POST', 'application/json', url, data);
}

/**
requestDelete(url, data)
A shortcut for `request('delete', 'application/json', url, data)`
*/

export function requestDelete(url, data) {
	return request('DELETE', 'application/json', url, data);
}

/**
throttledRequest(type, mimetype, url)
*/

function ignoreAbortError(error) {
	// Swallow AbortErrors, since we generate one every time we use
	// the AbortController.
	if (error.name === 'AbortError') {
		console.log('Request aborted by throttle. Nothing to worry about.');

		// JS promises have no machanism to conditionally catch different
		// types of error – throw undefined to fall through to the next
		// catch without a value.
		throw undefined;
	}

	// Rethrow all other errors
	throw error;
}

export function throttledRequest(type, mimetype, url) {
	var controller, data, promise;

    function then() {
        controller = undefined;
    }

	function send() {
        controller = new AbortController();
        var req = request(type, mimetype, url, data, controller);
        req.then(then);
        promise = undefined;
        data    = undefined;
        return req;
	}

    return function(object) {
        data = object;

        if (promise) {
            return promise;
        }

        // Cancel previous request
        if (controller) {
			controller.abort();
            controller = undefined;
		}

        // Batch requests to ticks
        return promise = Promise
        .resolve()
        .then(send)
        .catch(ignoreAbortError);
    };
}
<commit_msg>Requets docs<commit_after>import { choose, id } from '../../fn/module.js';

const assign = Object.assign;

/*
config

```{
	headers:    fn(data),    // Must return an object with properties to add to the header
	body:       fn(data),    // Must return an object to send as data
	onresponse: function(response)
}```
*/

export const config = {
    // Takes data, returns headers
	headers: function(data) { return {}; },

	// Takes data (can be FormData object or plain object), returns data
	body: id,

	// Takes response, returns response
	onresponse: function(response) {
		// If redirected, navigate the browser away from here. Can get
		// annoying when receiving 404s, maybe not a good default...
		if (response.redirected) {
			window.location = response.url;
			return;
		}

		return response;
	}
};

const createHeaders = choose({
	'application/x-www-form-urlencoded': function(headers) {
		return assign(headers, {
			"Content-Type": 'application/x-www-form-urlencoded',
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'application/json': function(headers) {
		return assign(headers, {
			"Content-Type": "application/json; charset=utf-8",
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'multipart/form-data': function(headers) {
		return assign(headers, {
			"Content-Type": 'multipart/form-data',
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'audio/wav': function(headers) {
		return assign(headers, {
			"Content-Type": 'audio/wav',
			"X-Requested-With": "XMLHttpRequest"
		});
	},

	'default': function(headers) {
		return assign(headers, {
			"Content-Type": 'application/x-www-form-urlencoded',
			"X-Requested-With": "XMLHttpRequest"
		});
	}
});

const createBody = choose({
	'application/json': function(data) {
		return data.get ?
			formDataToJSON(data) :
			JSON.stringify(data);
	},

	'application/x-www-form-urlencoded': function(data) {
		return data.get ?
			formDataToQuery(data) :
			dataToQuery(data) ;
	},

	'multipart/form-data': function(data) {
		// Mmmmmhmmm?
		return data.get ?
            data :
            dataToFormData(data) ;
	}
});

function formDataToJSON(formData) {
	return JSON.stringify(
		// formData.entries() is an iterator, not an array
		Array
		.from(formData.entries())
		.reduce(function(output, entry) {
			output[entry[0]] = entry[1];
			return output;
		}, {})
	);
}

function formDataToQuery(data) {
	return new URLSearchParams(data).toString();
}

function dataToQuery(data) {
	return Object.keys(data).reduce((params, key) => {
		params.append(key, data[key]);
		return params;
	}, new URLSearchParams());
}

function dataToFormData(data) {
    throw new Error('TODO: dataToFormData(data)');
}

function urlFromData(url, data) {
	// Form data
	return data instanceof FormData ?
		url + '?' + formDataToQuery(data) :
		url + '?' + dataToQuery(data) ;
}

function createOptions(method, mimetype, data, controller) {
	return method === 'GET' ? {
		method:  method,
		headers: createHeaders(mimetype, config.headers ? config.headers(data) : {}),
		credentials: 'same-origin',
		signal: controller && controller.signal
	} : {
		method:  method,
		// Process headers before body, allowing us to read a CSRFToken,
        // which may be in data, in createHeaders() before removing it
        // from data in body().
		headers: createHeaders(mimetype, config.headers ? config.headers(data) : {}),
		body:    createBody(mimetype, config.body ? config.body(data) : data),
		credentials: 'same-origin',
		signal: controller && controller.signal
	} ;
}

const responders = {
	'text/html': respondText,
	'application/json': respondJSON,
	'multipart/form-data': respondForm,
	'application/x-www-form-urlencoded': respondForm,
	'audio': respondBlob,
	'audio/wav': respondBlob,
	'audio/m4a': respondBlob
};

function respondBlob(response) {
	return response.blob();
}

function respondJSON(response) {
	return response.json();
}

function respondForm(response) {
	return response.formData();
}

function respondText(response) {
	return response.text();
}

function respond(response) {
	if (config.onresponse) {
		response = config.onresponse(response);
	}

	if (!response.ok) {
		throw new Error(response.statusText + '');
	}

	// Get mimetype from Content-Type, remembering to hoik off any
	// parameters first
	const mimetype = response.headers
	.get('Content-Type')
	.replace(/\;.*$/, '');

	return responders[mimetype](response);
}

/**
request(type, mimetype, url, data)

Uses `fetch()` to send a request to `url`. Where `type` is `"GET"`, `data` is
serialised and appended to the URL, otherwise it is sent as a payload
conforming to the given `mimetype`.
**/

export default function request(type = 'GET', mimetype = 'application/json', url, data) {
	const method = type.toUpperCase();

	// If this is a GET and there is data, append data to the URL query string
	if (method === 'GET' && data) {
		url = urlFromData(url, data);
	}

	// param[4] is an optional abort controller
	return fetch(url, createOptions(method, mimetype, data, arguments[4]))
	.then(respond);
}

/**
requestGet(url)
A shortcut for `request('get', 'application/json', url)`
**/

export function requestGet(url) {
	return request('GET', 'application/json', url, {});
}

/**
requestPatch(url, data)
A shortcut for `request('patch', 'application/json', url, data)`
**/

export function requestPatch(url, data) {
	return request('PATCH', 'application/json', url, data);
}

/**
requestPost(url, data)
A shortcut for `request('post', 'application/json', url, data)`
**/

export function requestPost(url, data) {
	return request('POST', 'application/json', url, data);
}

/**
requestDelete(url, data)
A shortcut for `request('delete', 'application/json', url, data)`
**/

export function requestDelete(url, data) {
	return request('DELETE', 'application/json', url, data);
}

/*
throttledRequest(type, mimetype, url)
*/

function ignoreAbortError(error) {
	// Swallow AbortErrors, since we generate one every time we use
	// the AbortController.
	if (error.name === 'AbortError') {
		console.log('Request aborted by throttle. Nothing to worry about.');

		// JS promises have no machanism to conditionally catch different
		// types of error – throw undefined to fall through to the next
		// catch without a value.
		throw undefined;
	}

	// Rethrow all other errors
	throw error;
}

export function throttledRequest(type, mimetype, url) {
	var controller, data, promise;

    function then() {
        controller = undefined;
    }

	function send() {
        controller = new AbortController();
        var req = request(type, mimetype, url, data, controller);
        req.then(then);
        promise = undefined;
        data    = undefined;
        return req;
	}

    return function(object) {
        data = object;

        if (promise) {
            return promise;
        }

        // Cancel previous request
        if (controller) {
			controller.abort();
            controller = undefined;
		}

        // Batch requests to ticks
        return promise = Promise
        .resolve()
        .then(send)
        .catch(ignoreAbortError);
    };
}
<|endoftext|>