<commit_before>/*
 * This script runs through a few checks and lets us know if something is off
 */

import Promise from 'bluebird';
import models, { sequelize } from '../../server/models';
import emailLib from '../../server/lib/email';
import { getSubscriptionsList } from '../../server/paymentProviders/stripe/gateway';

const VERBOSE = true;
let result = '';
let start;
let issuesFound = false;

const done = (err) => {
  if (err) result = result.concat('err', err);
  result = result.concat("\n\nTotal time taken: ", new Date() - start, "ms")
  console.log(result);
  console.log('\ndone!\n');``
  const subject = `${issuesFound ? '❌' : '✅'} Daily ledger health report - ${(new Date()).toLocaleDateString()}`;
  return emailLib.sendMessage(
    'ops@opencollective.com',
    subject,
    '', {
      bcc: ' ',
      text: result
    })
    .then(process.exit)
    .catch(console.error)
}

/* Helper functions */
const judgment = (value, goodFunc) => {
  if ((goodFunc && goodFunc(value)) || (!goodFunc && value === 0)) {
    return '✅'
  } else {
    issuesFound = true;
    return '❌'
  }
}

const header = (str) => {
  const newString = `\n>>> ${str}\n`;
  result = result.concat(newString);
  console.log(newString)
}

const subHeader = (str, value, goodFunc) => {
  const newString = `\t${judgment(value, goodFunc)}  ${str}: ${value}\n`;
  result = result.concat(newString);
  console.log(newString);
}

const subsubHeader = (str, value, goodFunc) => {
  const newString = `\t\t${judgment(value, goodFunc)}  ${str}: ${value}\n`;
  result = result.concat(newString);
  console.log(newString);
}

const verboseData = (values, mapFunction) => {
  const mapFunc = mapFunction || (o => o);
  if (VERBOSE && values.length > 0) {
    const slice = 5;
    const output = values.slice(0, slice).map(mapFunc);
    output.forEach(v => {
      const newString = `\t\t▫ ${JSON.stringify(v)}\n`;
      result = result.concat(newString)
      console.log(newString);
    })
    if (values.length > 10) {
      const newString = `\t\t... and ${values.length - slice} more`;
      result = result.concat(newString);
      console.log(newString);
    }
  }
}

// Used to fetch external data that only comes in batches
// Like stripe subscription list, only 100 at a time
const fetchAll = (func, options) => {
  return func(options)
    .then(results => {
      // if fetched less than the limit, stop fetching
      if (results.data.length < options.limit) {
        return results.data;
      }
      // otherwise, keep fetching from the end of last set of results
      options.startingAfter = results.data.slice(-1)[0].id;
      return fetchAll(func, options)
        .then(result2 => results.data.concat(result2))
    })
}

/* **** functions that check for various things **** */

const checkHostsUserOrOrg = () => {

  header('Checking Hosts must be USER or ORG');

  const hostErrors = [];

  // Check that a Host is a User or an ORG
  return sequelize.query(`
    WITH hosts as (SELECT distinct("HostCollectiveId") from "Collectives")

    SELECT id, type, slug from "Collectives"
    WHERE id IN (SELECT * FROM hosts);
    `, { type: sequelize.QueryTypes.SELECT})
    .then(hostCollectives => {
      subHeader('Hosts found', hostCollectives.length, h => h > 0);
      return hostCollectives
    })
    .each(hostCollective => {
      if (hostCollective.type !== 'USER' && hostCollective.type !== 'ORGANIZATION') {
        hostErrors.push(hostCollective);
      }
    })
    .then(() => {
      subHeader('Hosts found with incorrect type', hostErrors.length);
      verboseData(hostErrors, h => Object.assign({slug: h.slug, type: h.type}));
    });
}

// Ensure all Collectives are setup properly
const checkHostCollectives = () => {

  header('Checking Host Collectives')

  // Check that a collective is not setup to host itself or be it's own parentCollectiveId
  return models.Collective.findAll({
    where: {
      HostCollectiveId: {
        $col: 'id'
      }
    }
  })
  .then(selfReferencingHosts => {
    subHeader('Self-referencing Hosts found', selfReferencingHosts.length);
    verboseData(selfReferencingHosts, h => Object.assign({slug: h.slug, id: h.id}));
  })
}

const checkUsersAndOrgs = () => {

  header('Checking USER and ORG Collectives');

  // Check that no User or ORG has a HostCollectiveId or ParentCollectiveId
  return models.Collective.findAll({
    where: {
      type: {
        $or: ['USER', 'ORGANIZATION']
      },
      HostCollectiveId: {
        $ne: null
      }
    }
  })
  .then(collectives => {
    subHeader('USER or ORGs found with HostCollectiveId', collectives.length);
    verboseData(collectives, c => Object.assign({slug: c.slug, HostCollectiveId: c.HostCollectiveId}));
  })
  // TODO: Check that no non-USER Collective is directly linked to a USER
  .then(() => models.User.findAll({
    attributes: ['CollectiveId']
  }))
  .then(userCollectives => models.Collective.findAll({
    where: {
      id: {
        $in: userCollectives.map(u => u.CollectiveId)
      },
      type: {
        $ne: 'USER'
      }
    }
  }))
  .then(improperlyLinkedCollectives => {
    subHeader('Non-User collectives that are linked to a USER', improperlyLinkedCollectives.length);
    verboseData(improperlyLinkedCollectives, c => Object.assign({id: c.id, slug: c.slug }));
  })
}

const checkMembers = () => {
  header('Checking Members table');

  return models.Member.findAll({
    where: {
      MemberCollectiveId: {
        $col: 'CollectiveId'
      }
    }
  })
  .then(circularMembers => {
    subHeader('Members with CollectiveId = MemberCollectiveId', circularMembers.length);
    verboseData(circularMembers, cm => cm.id);
  })
}

/* DISABLED, matching funds fail this check
// TODO: Find a way to filter out matching funds
// Check orders
const checkOrders = () => {

  header('Check orders');

  // Check that FromCollectiveId on an Order matches all Transactions
  const brokenOrders = [];
  let orders, transactions;
  // ignores prepaid orders that have 4 transactions with different fromCollectiveId
  return sequelize.query(`
    SELECT o.id from "Orders" o
    LEFT JOIN "PaymentMethods" pm on o."PaymentMethodId" = pm.id
    WHERE o."deletedAt" is null AND o."processedAt" is not null AND o."CollectiveId" != 1 AND pm.service not ilike 'opencollective' AND pm.type not ilike 'prepaid'
    `, { type: sequelize.QueryTypes.SELECT
    })
    .then(o => {
      orders = o;
      subHeader('orders found', orders.length, o => o > 0);
    })
    .then(() => sequelize.query(`
      SELECT distinct("FromCollectiveId"), "OrderId" from "Transactions"
      WHERE type LIKE 'CREDIT' AND "deletedAt" is null
      `, {
        type: sequelize.QueryTypes.SELECT
      }))
    .then(txns => {
      transactions = txns;
    })
    .then(() => orders)
    .each(order => {
      const fromCollectiveIds = transactions.filter(txn => txn.OrderId === order.id)
      if (fromCollectiveIds.length > 1) {
        brokenOrders.push(order)
      }
      return Promise.resolve();
    })
    .then(() => {
      subHeader('orders found with mismatched FromCollectiveId', brokenOrders.length);
      verboseData(brokenOrders, o => o.id);
    })
} */

// Check expenses
const checkExpenses = () => {

  header('Check expenses');

  // Check that there are no expenses marked as "PAID" and without transaction entries
  return sequelize.query(`
    SELECT
      e.id AS id
    FROM "Expenses" e
    LEFT JOIN "Transactions" t ON t."ExpenseId" = e.id
    WHERE e.status ILIKE 'paid' AND t.id IS NULL AND  e."deletedAt" IS NULL
    ORDER BY "ExpenseId" DESC, e."updatedAt"
    `, { type: sequelize.QueryTypes.SELECT
    })
    .then(expenses => {
      subHeader('Paid expenses found without transactions', expenses.length);
      verboseData(expenses, e => Object.assign({id: e.id}));
    })
}

// Check all transactions
const checkTransactions = () => {

  header('Checking Transactions...')

  // Check every transaction has a "FromCollectiveId"
  return models.Transaction.count({
    where: {
      FromCollectiveId: {
        $eq: null
      }
    }
  })
  .then(txsWithoutFromCollectiveId => {
    subHeader('Transactions without `FromCollectiveId`', txsWithoutFromCollectiveId);
  })

  // Check no transaction has same "FromCollectiveId" and "CollectiveId"
  .then(() => models.Transaction.findAll({
    where: {
      CollectiveId: {
        $col: 'FromCollectiveId'
      }
    }
  }))
  .then(circularTxs => {
    subHeader('Transactions with same source and destination', circularTxs.length)
    verboseData(circularTxs, t => Object.assign({id: t.id}));
  })

  // check no transactions without TransactionGroup
  .then(() => models.Transaction.count({
    where: {
      TransactionGroup: {
        $eq: null
      }
    }
  }))
  .then(txnsWithoutTransactionGroup => {
    subHeader('Transactions without `TransactionGroup`', txnsWithoutTransactionGroup)
  })

  // Check every Order has even number of entries
  .then(() => sequelize.query(`
    SELECT "OrderId" FROM "Transactions"
        WHERE "OrderId" IS NOT NULL and "deletedAt" is null
          GROUP BY "OrderId"
          HAVING COUNT(*) % 2 != 0
    `, {type: sequelize.QueryTypes.SELECT}))
  .then(oddOrderIds => {
    subHeader('Orders with odd (not multiple of 2) number of transactions', oddOrderIds.length)
  })

  // Check every Expense has a double Entry
  .then(() => sequelize.query(`
    SELECT "ExpenseId" FROM "Transactions"
        WHERE "ExpenseId" IS NOT NULL and "deletedAt" is null
          GROUP BY "ExpenseId"
          HAVING COUNT(*) != 2
    `, {type: sequelize.QueryTypes.SELECT}))
  .then(oddExpenseIds => {
    subHeader('Expenses with less than or more than 2 transactions', oddExpenseIds.length)
    verboseData(oddExpenseIds)
  })

  // Check all TransactionGroups have two entries, one CREDIT and one DEBIT
  .then(() => sequelize.query(`
    SELECT "TransactionGroup" FROM "Transactions"
        WHERE "TransactionGroup" IS NOT NULL and "deletedAt" is null
          GROUP BY "TransactionGroup"
          HAVING COUNT(*) != 2
    `, {type: sequelize.QueryTypes.SELECT}))
  .then(oddTxnGroups => {
    subHeader('Transaction groups that are not pairs', oddTxnGroups.length)
    verboseData(oddTxnGroups)
  })

  // Check no transactions without either an Expense or Order
  .then(() => models.Transaction.findAll({
    where: {
      OrderId: {
        $eq: null
      },
      ExpenseId: {
        $eq: null
      }
    }
  }))
  .then(txnsWithoutOrderOrExpenses => {
    // 600 very old txns. Not worth fixing right now.
    subHeader('Transactions without OrderId or ExpenseId', txnsWithoutOrderOrExpenses.length-600);
    // if (VERBOSE)
      // txnsWithoutOrderOrExpenses.map(t => Object.assign({id: t.id}));
  })

  // Check that various fees and amounts add up
  // TODO
}

const checkCollectiveBalance = () => {

  const brokenCollectives = [];
  header('Checking balance of each (non-USER, non-ORG) collective');
  return models.Collective.findAll({
    attributes: [ 'id' ],
    where: {
      $or: [{type: 'COLLECTIVE'}, {type: 'EVENT'}],
      id: {
        $notIn: [7, 34]
      }
    }
  })
  .then(collectives => {
    subHeader('Collectives found', collectives.length, l => l > 0);
    return collectives;
  })
  .each(collective => {
    return collective.getBalance()
      .then(balance => {
        if (balance < 0) {
          brokenCollectives.push(collective)
        }
        return Promise.resolve();
      })
  })
  .then(() => {
    subHeader('Collectives with negative balance: ', brokenCollectives.length);
    verboseData(brokenCollectives, c => Object.assign({id: c.id, slug: c.slug}))
  })

}

const checkSubscriptionsFromStripeForAHost = (hostCollectiveId) => {
  let stripeAccount;
  let stripeSubscriptionsList = [];

  // for each of those stripe accounts, fetch each subscription and verify
  return models.ConnectedAccount.findAll({ where: {CollectiveId: hostCollectiveId}})
  .then(stripeAccounts => {
    if (!stripeAccounts || stripeAccounts.length === 0) {
      throw new Error('stripe account not found');
    }

    if (stripeAccounts.length > 1) {
      throw new Error('More than one old stripe account found');
    }
    stripeAccount = stripeAccounts[0];
  })
  .then(() => fetchAll(getSubscriptionsList.bind(null, stripeAccount), { limit: 100 }))
  .then(stripeSubscriptions => {
    subHeader('HostCollectiveId', hostCollectiveId, h => h > 0);
    subsubHeader('Stripe subscriptions found: ', stripeSubscriptions.length, s => s > 0);
    stripeSubscriptionsList = stripeSubscriptions.map(x => x.id);
  })
  .then(() => models.Subscription.findAll({
    where: {
      stripeSubscriptionId: {
        $in: stripeSubscriptionsList
      }
    },
    attributes: [ 'id', 'stripeSubscriptionId' ]
  }))
  .then(ocSubscriptions => {
    const ocList = ocSubscriptions.map(x => x.stripeSubscriptionId);
    subsubHeader('OC Subscriptions found: ', ocList.length, s => s > 0)

    const ocListSet = new Set(ocList);
    return stripeSubscriptionsList.filter(n => !ocListSet.has(n));
  })
  .then(missingSubs => {
    subsubHeader(`Subscriptions missing in our DB on Host Collective Id ${hostCollectiveId}`, missingSubs.length);
    verboseData(missingSubs)
  })

}

const checkSubscriptionsOnStripe = () => {

  header('Checking that all subscriptions on Stripe are present in our database');

  // These are all the hosts that are only used for subscriptions on OC, so easy to test
  const hostCollectiveIds = [8674, 9806, 9807, 11004, 11049];

  return Promise.map(hostCollectiveIds, checkSubscriptionsFromStripeForAHost);
}

const run = () => {
  console.log('\nStarting check_ledger_health script...')
  start = new Date();

  return checkHostsUserOrOrg()
  .then(() => checkHostCollectives())
  .then(() => checkUsersAndOrgs())
  .then(() => checkMembers())
  //.then(() => checkOrders())
  .then(() => checkExpenses())
  .then(() => checkTransactions())
  .then(() => checkCollectiveBalance())
  .then(() => checkSubscriptionsOnStripe())
  .then(() => done())
  .catch(done)
}

run();<commit_msg>check_ledger_health: Remove stripe subscription health check (#1126)<commit_after>/*
 * This script runs through a few checks and lets us know if something is off
 */

import Promise from 'bluebird';
import models, { sequelize } from '../../server/models';
import emailLib from '../../server/lib/email';

const VERBOSE = true;
let result = '';
let start;
let issuesFound = false;

const done = (err) => {
  if (err) result = result.concat('err', err);
  result = result.concat("\n\nTotal time taken: ", new Date() - start, "ms")
  console.log(result);
  console.log('\ndone!\n');``
  const subject = `${issuesFound ? '❌' : '✅'} Daily ledger health report - ${(new Date()).toLocaleDateString()}`;
  return emailLib.sendMessage(
    'ops@opencollective.com',
    subject,
    '', {
      bcc: ' ',
      text: result
    })
    .then(process.exit)
    .catch(console.error)
}

/* Helper functions */
const judgment = (value, goodFunc) => {
  if ((goodFunc && goodFunc(value)) || (!goodFunc && value === 0)) {
    return '✅'
  } else {
    issuesFound = true;
    return '❌'
  }
}

const header = (str) => {
  const newString = `\n>>> ${str}\n`;
  result = result.concat(newString);
  console.log(newString)
}

const subHeader = (str, value, goodFunc) => {
  const newString = `\t${judgment(value, goodFunc)}  ${str}: ${value}\n`;
  result = result.concat(newString);
  console.log(newString);
}

const verboseData = (values, mapFunction) => {
  const mapFunc = mapFunction || (o => o);
  if (VERBOSE && values.length > 0) {
    const slice = 5;
    const output = values.slice(0, slice).map(mapFunc);
    output.forEach(v => {
      const newString = `\t\t▫ ${JSON.stringify(v)}\n`;
      result = result.concat(newString)
      console.log(newString);
    })
    if (values.length > 10) {
      const newString = `\t\t... and ${values.length - slice} more`;
      result = result.concat(newString);
      console.log(newString);
    }
  }
}

// Used to fetch external data that only comes in batches
// Like stripe subscription list, only 100 at a time
const fetchAll = (func, options) => {
  return func(options)
    .then(results => {
      // if fetched less than the limit, stop fetching
      if (results.data.length < options.limit) {
        return results.data;
      }
      // otherwise, keep fetching from the end of last set of results
      options.startingAfter = results.data.slice(-1)[0].id;
      return fetchAll(func, options)
        .then(result2 => results.data.concat(result2))
    })
}

/* **** functions that check for various things **** */

const checkHostsUserOrOrg = () => {

  header('Checking Hosts must be USER or ORG');

  const hostErrors = [];

  // Check that a Host is a User or an ORG
  return sequelize.query(`
    WITH hosts as (SELECT distinct("HostCollectiveId") from "Collectives")

    SELECT id, type, slug from "Collectives"
    WHERE id IN (SELECT * FROM hosts);
    `, { type: sequelize.QueryTypes.SELECT})
    .then(hostCollectives => {
      subHeader('Hosts found', hostCollectives.length, h => h > 0);
      return hostCollectives
    })
    .each(hostCollective => {
      if (hostCollective.type !== 'USER' && hostCollective.type !== 'ORGANIZATION') {
        hostErrors.push(hostCollective);
      }
    })
    .then(() => {
      subHeader('Hosts found with incorrect type', hostErrors.length);
      verboseData(hostErrors, h => Object.assign({slug: h.slug, type: h.type}));
    });
}

// Ensure all Collectives are setup properly
const checkHostCollectives = () => {

  header('Checking Host Collectives')

  // Check that a collective is not setup to host itself or be it's own parentCollectiveId
  return models.Collective.findAll({
    where: {
      HostCollectiveId: {
        $col: 'id'
      }
    }
  })
  .then(selfReferencingHosts => {
    subHeader('Self-referencing Hosts found', selfReferencingHosts.length);
    verboseData(selfReferencingHosts, h => Object.assign({slug: h.slug, id: h.id}));
  })
}

const checkUsersAndOrgs = () => {

  header('Checking USER and ORG Collectives');

  // Check that no User or ORG has a HostCollectiveId or ParentCollectiveId
  return models.Collective.findAll({
    where: {
      type: {
        $or: ['USER', 'ORGANIZATION']
      },
      HostCollectiveId: {
        $ne: null
      }
    }
  })
  .then(collectives => {
    subHeader('USER or ORGs found with HostCollectiveId', collectives.length);
    verboseData(collectives, c => Object.assign({slug: c.slug, HostCollectiveId: c.HostCollectiveId}));
  })
  // TODO: Check that no non-USER Collective is directly linked to a USER
  .then(() => models.User.findAll({
    attributes: ['CollectiveId']
  }))
  .then(userCollectives => models.Collective.findAll({
    where: {
      id: {
        $in: userCollectives.map(u => u.CollectiveId)
      },
      type: {
        $ne: 'USER'
      }
    }
  }))
  .then(improperlyLinkedCollectives => {
    subHeader('Non-User collectives that are linked to a USER', improperlyLinkedCollectives.length);
    verboseData(improperlyLinkedCollectives, c => Object.assign({id: c.id, slug: c.slug }));
  })
}

const checkMembers = () => {
  header('Checking Members table');

  return models.Member.findAll({
    where: {
      MemberCollectiveId: {
        $col: 'CollectiveId'
      }
    }
  })
  .then(circularMembers => {
    subHeader('Members with CollectiveId = MemberCollectiveId', circularMembers.length);
    verboseData(circularMembers, cm => cm.id);
  })
}

/* DISABLED, matching funds fail this check
// TODO: Find a way to filter out matching funds
// Check orders
const checkOrders = () => {

  header('Check orders');

  // Check that FromCollectiveId on an Order matches all Transactions
  const brokenOrders = [];
  let orders, transactions;
  // ignores prepaid orders that have 4 transactions with different fromCollectiveId
  return sequelize.query(`
    SELECT o.id from "Orders" o
    LEFT JOIN "PaymentMethods" pm on o."PaymentMethodId" = pm.id
    WHERE o."deletedAt" is null AND o."processedAt" is not null AND o."CollectiveId" != 1 AND pm.service not ilike 'opencollective' AND pm.type not ilike 'prepaid'
    `, { type: sequelize.QueryTypes.SELECT
    })
    .then(o => {
      orders = o;
      subHeader('orders found', orders.length, o => o > 0);
    })
    .then(() => sequelize.query(`
      SELECT distinct("FromCollectiveId"), "OrderId" from "Transactions"
      WHERE type LIKE 'CREDIT' AND "deletedAt" is null
      `, {
        type: sequelize.QueryTypes.SELECT
      }))
    .then(txns => {
      transactions = txns;
    })
    .then(() => orders)
    .each(order => {
      const fromCollectiveIds = transactions.filter(txn => txn.OrderId === order.id)
      if (fromCollectiveIds.length > 1) {
        brokenOrders.push(order)
      }
      return Promise.resolve();
    })
    .then(() => {
      subHeader('orders found with mismatched FromCollectiveId', brokenOrders.length);
      verboseData(brokenOrders, o => o.id);
    })
} */

// Check expenses
const checkExpenses = () => {

  header('Check expenses');

  // Check that there are no expenses marked as "PAID" and without transaction entries
  return sequelize.query(`
    SELECT
      e.id AS id
    FROM "Expenses" e
    LEFT JOIN "Transactions" t ON t."ExpenseId" = e.id
    WHERE e.status ILIKE 'paid' AND t.id IS NULL AND  e."deletedAt" IS NULL
    ORDER BY "ExpenseId" DESC, e."updatedAt"
    `, { type: sequelize.QueryTypes.SELECT
    })
    .then(expenses => {
      subHeader('Paid expenses found without transactions', expenses.length);
      verboseData(expenses, e => Object.assign({id: e.id}));
    })
}

// Check all transactions
const checkTransactions = () => {

  header('Checking Transactions...')

  // Check every transaction has a "FromCollectiveId"
  return models.Transaction.count({
    where: {
      FromCollectiveId: {
        $eq: null
      }
    }
  })
  .then(txsWithoutFromCollectiveId => {
    subHeader('Transactions without `FromCollectiveId`', txsWithoutFromCollectiveId);
  })

  // Check no transaction has same "FromCollectiveId" and "CollectiveId"
  .then(() => models.Transaction.findAll({
    where: {
      CollectiveId: {
        $col: 'FromCollectiveId'
      }
    }
  }))
  .then(circularTxs => {
    subHeader('Transactions with same source and destination', circularTxs.length)
    verboseData(circularTxs, t => Object.assign({id: t.id}));
  })

  // check no transactions without TransactionGroup
  .then(() => models.Transaction.count({
    where: {
      TransactionGroup: {
        $eq: null
      }
    }
  }))
  .then(txnsWithoutTransactionGroup => {
    subHeader('Transactions without `TransactionGroup`', txnsWithoutTransactionGroup)
  })

  // Check every Order has even number of entries
  .then(() => sequelize.query(`
    SELECT "OrderId" FROM "Transactions"
        WHERE "OrderId" IS NOT NULL and "deletedAt" is null
          GROUP BY "OrderId"
          HAVING COUNT(*) % 2 != 0
    `, {type: sequelize.QueryTypes.SELECT}))
  .then(oddOrderIds => {
    subHeader('Orders with odd (not multiple of 2) number of transactions', oddOrderIds.length)
  })

  // Check every Expense has a double Entry
  .then(() => sequelize.query(`
    SELECT "ExpenseId" FROM "Transactions"
        WHERE "ExpenseId" IS NOT NULL and "deletedAt" is null
          GROUP BY "ExpenseId"
          HAVING COUNT(*) != 2
    `, {type: sequelize.QueryTypes.SELECT}))
  .then(oddExpenseIds => {
    subHeader('Expenses with less than or more than 2 transactions', oddExpenseIds.length)
    verboseData(oddExpenseIds)
  })

  // Check all TransactionGroups have two entries, one CREDIT and one DEBIT
  .then(() => sequelize.query(`
    SELECT "TransactionGroup" FROM "Transactions"
        WHERE "TransactionGroup" IS NOT NULL and "deletedAt" is null
          GROUP BY "TransactionGroup"
          HAVING COUNT(*) != 2
    `, {type: sequelize.QueryTypes.SELECT}))
  .then(oddTxnGroups => {
    subHeader('Transaction groups that are not pairs', oddTxnGroups.length)
    verboseData(oddTxnGroups)
  })

  // Check no transactions without either an Expense or Order
  .then(() => models.Transaction.findAll({
    where: {
      OrderId: {
        $eq: null
      },
      ExpenseId: {
        $eq: null
      }
    }
  }))
  .then(txnsWithoutOrderOrExpenses => {
    // 600 very old txns. Not worth fixing right now.
    subHeader('Transactions without OrderId or ExpenseId', txnsWithoutOrderOrExpenses.length-600);
    // if (VERBOSE)
      // txnsWithoutOrderOrExpenses.map(t => Object.assign({id: t.id}));
  })

  // Check that various fees and amounts add up
  // TODO
}

const checkCollectiveBalance = () => {

  const brokenCollectives = [];
  header('Checking balance of each (non-USER, non-ORG) collective');
  return models.Collective.findAll({
    attributes: [ 'id' ],
    where: {
      $or: [{type: 'COLLECTIVE'}, {type: 'EVENT'}],
      id: {
        $notIn: [7, 34]
      }
    }
  })
  .then(collectives => {
    subHeader('Collectives found', collectives.length, l => l > 0);
    return collectives;
  })
  .each(collective => {
    return collective.getBalance()
      .then(balance => {
        if (balance < 0) {
          brokenCollectives.push(collective)
        }
        return Promise.resolve();
      })
  })
  .then(() => {
    subHeader('Collectives with negative balance: ', brokenCollectives.length);
    verboseData(brokenCollectives, c => Object.assign({id: c.id, slug: c.slug}))
  })
}

const run = () => {
  console.log('\nStarting check_ledger_health script...')
  start = new Date();

  return checkHostsUserOrOrg()
  .then(() => checkHostCollectives())
  .then(() => checkUsersAndOrgs())
  .then(() => checkMembers())
  //.then(() => checkOrders())
  .then(() => checkExpenses())
  .then(() => checkTransactions())
  .then(() => checkCollectiveBalance())
  .then(() => done())
  .catch(done)
}

run();
<|endoftext|>


<commit_before>/* global $, _, msglist:true */
/* global phiTitle, phiBar, phiQ, setup, JAM, beforeSpace */

msglist = function(echotag){ /* jshint indent:false */

var fillRowFromHeader = function($msgRow, filledCallback){
   var GUI = require('nw.gui');
   var nwClipboard = GUI.Clipboard.get();
   echobase.readHeader($msgRow.data('number'), function(err, header){
      if( err ){
         $msgRow.find(
            '.msgFrom, .msgTo, .msgSubj, .msgDateTime'
         ).html('FAIL');
         filledCallback();
         return;
      }
      var decoded = echobase.decodeHeader(header);
      var msgURL;
      $msgRow.html([
         '' + $msgRow.data('number') + '	',
         '' + _.escapeHTML(decoded.from) + '	',
         '' + _.escapeHTML(decoded.to) + '	',
         '' + _.escapeHTML(decoded.subj) + '	',
         '<nobr>',
         decoded.origTime[0], '-',
         _(decoded.origTime[1]).pad(2, '0'), '-',
         _(decoded.origTime[2]).pad(2, '0'),
         '</nobr> <nobr>',
         _(decoded.origTime[3]).pad(2, '0'), ':',
         _(decoded.origTime[4]).pad(2, '0'), ':',
         _(decoded.origTime[5]).pad(2, '0'),
         '</nobr>	'
      ].join(''));
      if( decoded.msgid ){
         msgURL = [
            'area://',
            encodeURIComponent(echotag),
            '/?msgid=',
            encodeURIComponent(decoded.msgid),
            '&time=',
            encodeURIComponent(decoded.origTime[0])
         ].join('').replace('%20', '+');
      } else {
         msgURL = [
            'area://',
            encodeURIComponent(echotag),
            '/?time=',
            encodeURIComponent(decoded.origTime[0]), '/',
            encodeURIComponent(_(decoded.origTime[1]).pad(2, '0')), '/',
            encodeURIComponent(_(decoded.origTime[2]).pad(2, '0')), 'T',
            encodeURIComponent(_(decoded.origTime[3]).pad(2, '0')), ':',
            encodeURIComponent(_(decoded.origTime[4]).pad(2, '0')), ':',
            encodeURIComponent(_(decoded.origTime[5]).pad(2, '0'))
         ].join('').replace('%20', '+');
      }
      $msgRow.data('msgURL', msgURL).on('click', function(){
         phiBar.open( $(this).data('msgURL') );
      }).on('contextmenu', function(e){
         var contextMenu = new GUI.Menu();
         contextMenu.append(new GUI.MenuItem({
            'label': 'Copy FGHI URL',
            'click': function(){
               nwClipboard.set( $msgRow.data('msgURL') );
            }
         }));
         $msgRow.data('contextMenu', contextMenu);
         $msgRow.data('contextMenu').popup(
            e.originalEvent.x,
            e.originalEvent.y
         );
         return false;
      }).find('td').css('cursor', 'pointer');
      filledCallback();
   });
};

var msghdrActionQueue = function(){
   $('.msgList .msgRow').each(function(){
      var $row = $(this);
      phiQ.push(function(qNext){
         fillRowFromHeader($row, qNext);
      });
   });
};

var msghdrDelayedActionQueue = function(){
   $('.msgList .msgRow').each(function(){
      var $row = $(this);
      $row.on('scrollSpy:exit', function(){
         $(this).data('inscroll', false);
      }).on('scrollSpy:enter', function(){
         $(this).data('inscroll', true);
         phiQ.push(function(qNext){
            if( ! $row.data('inscroll') ){
               qNext();
               return;
            }
            fillRowFromHeader($row, function(){
               $row.off('scrollSpy:exit').off('scrollSpy:enter');
               qNext();
            });
         }).start();
      });
   }).scrollSpy();
};

phiTitle(echotag + ' - messages');
var lcEchotag = echotag.toLowerCase();

var echoNames = setup.areas.group('EchoArea').names();
var foundNames = echoNames.filter(function(echoName){
   return echoName.toLowerCase() === lcEchotag;
});

if( foundNames.length === 0 ){
   return phiBar.reportErrorHTML([
      'Sorry, the echomail area ',
      echotag,
      ' is not found on the system.'
   ].join(''));
}

var setupEchotag = foundNames[0];
var echoPath = beforeSpace(
   setup.areas.group('EchoArea').first(setupEchotag)
);
var echobase = JAM( echoPath );

var arrDesc = /-d "([^"]+?)"/.exec(
   setup.areas.group('EchoArea').first(setupEchotag)
);
var echoDesc;
if( arrDesc === null ){
   echoDesc = setupEchotag;
} else {
   echoDesc = arrDesc[1];
}

echobase.readJDX(function(err){
   if( err ) return phiBar.reportErrorHTML( _.escapeHTML('' + err) );

   var baseSize = echobase.size();
   var loadingRows;
   if( baseSize <= 250 ){
      loadingRows = [
         '	',
         '	',
         '	',
         '	'
      ].join('');
   } else { // do not spin in larger areas
      loadingRows = [
         '	',
         '	',
         '	',
         '	'
      ].join('');
   }

   $('#content').html('');
   var currMsg, $currTBody;
   for( currMsg = 1; currMsg <= baseSize; currMsg++ ){
      if( currMsg % 500 === 1 ){
         $currTBody = $([
            '
',
            '

',
            '',
            echoDesc,
            '	',
            '

',
            'Num	',
            'From	',
            'To	',
            'Subject	',
            'Date / time	',
            '

'
         ].join('')).appendTo('#content').find('tbody:last');
      }
      $(['
',
         '',
            currMsg,
         '	',
         loadingRows,
      '
'].join('')).data({
         'number': currMsg
      }).appendTo($currTBody);
   }

   if( baseSize <= 250 ){
      msghdrActionQueue();
   } else {
      msghdrDelayedActionQueue();
   }
   phiQ.start();
});

};<commit_msg>setTimeout() between large tables (preventing long freezes)<commit_after>/* global $, _, msglist:true */
/* global phiTitle, phiBar, phiQ, setup, JAM, beforeSpace */

msglist = function(echotag){ /* jshint indent:false */

var echobase, baseSize, loadingRows;

var fillRowFromHeader = function($msgRow, filledCallback){
   var GUI = require('nw.gui');
   var nwClipboard = GUI.Clipboard.get();
   echobase.readHeader($msgRow.data('number'), function(err, header){
      if( err ){
         $msgRow.find(
            '.msgFrom, .msgTo, .msgSubj, .msgDateTime'
         ).html('FAIL');
         filledCallback();
         return;
      }
      var decoded = echobase.decodeHeader(header);
      var msgURL;
      $msgRow.html([
         '' + $msgRow.data('number') + '	',
         '' + _.escapeHTML(decoded.from) + '	',
         '' + _.escapeHTML(decoded.to) + '	',
         '' + _.escapeHTML(decoded.subj) + '	',
         '<nobr>',
         decoded.origTime[0], '-',
         _(decoded.origTime[1]).pad(2, '0'), '-',
         _(decoded.origTime[2]).pad(2, '0'),
         '</nobr> <nobr>',
         _(decoded.origTime[3]).pad(2, '0'), ':',
         _(decoded.origTime[4]).pad(2, '0'), ':',
         _(decoded.origTime[5]).pad(2, '0'),
         '</nobr>	'
      ].join(''));
      if( decoded.msgid ){
         msgURL = [
            'area://',
            encodeURIComponent(echotag),
            '/?msgid=',
            encodeURIComponent(decoded.msgid),
            '&time=',
            encodeURIComponent(decoded.origTime[0])
         ].join('').replace('%20', '+');
      } else {
         msgURL = [
            'area://',
            encodeURIComponent(echotag),
            '/?time=',
            encodeURIComponent(decoded.origTime[0]), '/',
            encodeURIComponent(_(decoded.origTime[1]).pad(2, '0')), '/',
            encodeURIComponent(_(decoded.origTime[2]).pad(2, '0')), 'T',
            encodeURIComponent(_(decoded.origTime[3]).pad(2, '0')), ':',
            encodeURIComponent(_(decoded.origTime[4]).pad(2, '0')), ':',
            encodeURIComponent(_(decoded.origTime[5]).pad(2, '0'))
         ].join('').replace('%20', '+');
      }
      $msgRow.data('msgURL', msgURL).on('click', function(){
         phiBar.open( $(this).data('msgURL') );
      }).on('contextmenu', function(e){
         var contextMenu = new GUI.Menu();
         contextMenu.append(new GUI.MenuItem({
            'label': 'Copy FGHI URL',
            'click': function(){
               nwClipboard.set( $msgRow.data('msgURL') );
            }
         }));
         $msgRow.data('contextMenu', contextMenu);
         $msgRow.data('contextMenu').popup(
            e.originalEvent.x,
            e.originalEvent.y
         );
         return false;
      }).find('td').css('cursor', 'pointer');
      filledCallback();
   });
};

var buildMessageTable = function(initialNum, sizeLimit, callback){
   var currMsg, $currTBody;

   var finalMode = false;
   var finalLimit = initialNum + sizeLimit - 1;
   if( finalLimit >= baseSize ){
      finalMode = true;
      finalLimit = baseSize;
   }

   $currTBody = $([
      '
',
      '

',
      '',
      echoDesc,
      '	',
      '

',
      'Num	',
      'From	',
      'To	',
      'Subject	',
      'Date / time	',
      '

'
   ].join('')).appendTo('#content').find('tbody:last');

   for( currMsg = initialNum; currMsg <= finalLimit; currMsg++ ){
      $(['
',
         '',
            currMsg,
         '	',
         loadingRows,
      '
'].join('')).data({
         'number': currMsg
      }).appendTo($currTBody);
   }

   setTimeout(function(){
      if( finalMode ){
         callback();
      } else {
         buildMessageTable(initialNum + sizeLimit, sizeLimit, callback);
      }
   }, 1);
};

var msghdrActionQueue = function(){
   $('.msgList .msgRow').each(function(){
      var $row = $(this);
      phiQ.push(function(qNext){
         fillRowFromHeader($row, qNext);
      });
   });
};

var msghdrDelayedActionQueue = function(){
   $('.msgList .msgRow').each(function(){
      var $row = $(this);
      $row.on('scrollSpy:exit', function(){
         $(this).data('inscroll', false);
      }).on('scrollSpy:enter', function(){
         $(this).data('inscroll', true);
         phiQ.push(function(qNext){
            if( ! $row.data('inscroll') ){
               qNext();
               return;
            }
            fillRowFromHeader($row, function(){
               $row.off('scrollSpy:exit').off('scrollSpy:enter');
               qNext();
            });
         }).start();
      });
   }).scrollSpy();
};

phiTitle(echotag + ' - messages');
var lcEchotag = echotag.toLowerCase();

var echoNames = setup.areas.group('EchoArea').names();
var foundNames = echoNames.filter(function(echoName){
   return echoName.toLowerCase() === lcEchotag;
});

if( foundNames.length === 0 ){
   return phiBar.reportErrorHTML([
      'Sorry, the echomail area ',
      echotag,
      ' is not found on the system.'
   ].join(''));
}

var setupEchotag = foundNames[0];
var echoPath = beforeSpace(
   setup.areas.group('EchoArea').first(setupEchotag)
);
echobase = JAM( echoPath );

var arrDesc = /-d "([^"]+?)"/.exec(
   setup.areas.group('EchoArea').first(setupEchotag)
);
var echoDesc;
if( arrDesc === null ){
   echoDesc = setupEchotag;
} else {
   echoDesc = arrDesc[1];
}

echobase.readJDX(function(err){
   if( err ) return phiBar.reportErrorHTML( _.escapeHTML('' + err) );

   baseSize = echobase.size();
   if( baseSize <= 250 ){
      loadingRows = [
         '	',
         '	',
         '	',
         '	'
      ].join('');
   } else { // do not spin in larger areas
      loadingRows = [
         '	',
         '	',
         '	',
         '	'
      ].join('');
   }

   $('#content').html('');
   buildMessageTable(1, 500, function(){
      if( baseSize <= 250 ){
         msghdrActionQueue();
      } else {
         msghdrDelayedActionQueue();
      }
      phiQ.start();
   });
});

};<|endoftext|>


<commit_before>import React, {Component} from 'react';

import Delay from '../util/Delay.js';
import Location from '../util/Location.js';
import Time from '../util/Time.js';

class Movement extends Component {
    render() {
        const p = this.props,
            row = p.row,
            data = p.data,
            lid = p.lid,
            wtp = row.timetable.wtp,
            forecast = row.forecast,
            plat = forecast.plat;
        let c1 = 'expt', c2 = c1;

        if (row.cancelled) {
            c1 = 'can';
            c2 = 'cancelled';
        } else if (wtp)
            c1 = c2 = 'pass';
        else if (forecast.departed || forecast.arrived) {
            c1 = 'arr';
            c2 = 'arrived';
        }

        // Show train icon if we are at the required position
        let icon = lid === row.id && !forecast.departed ?  : null;

        let terminated = row.planned && row.planned.activity === 'TF', //row.type === 'DT',
            arrived = forecast.arrived && !forecast.departed,
            expected = !(forecast.arrived || forecast.departed);

        return

            {icon}

                <Location data={data} tiploc={row.tiploc}/>

                {row.cancelled ? 'Cancelled' : wtp ? 'Pass' : terminated ? "Terminated" : plat && (plat.suppressed && !forecast.departed) ? null : plat.plat}

                <Time time={forecast.time} terminated={terminated} arrived={arrived} expected={expected}/>

                <Delay delay={row.delay}/>

;

    }
}

export default Movement;
<commit_msg>Only show Terminated if this is the terminated early entry<commit_after>import React, {Component} from 'react';

import Delay from '../util/Delay.js';
import Location from '../util/Location.js';
import Time from '../util/Time.js';

class Movement extends Component {
    render() {
        const p = this.props,
            row = p.row,
            data = p.data,
            service = data.service,
            lid = p.lid,
            wtp = row.timetable.wtp,
            forecast = row.forecast,
            plat = forecast.plat;
        let c1 = 'expt', c2 = c1;

        if (row.cancelled) {
            c1 = 'can';
            c2 = 'cancelled';
        } else if (wtp)
            c1 = c2 = 'pass';
        else if (forecast.departed || forecast.arrived) {
            c1 = 'arr';
            c2 = 'arrived';
        }

        // Show train icon if we are at the required position
        let icon = lid === row.id && !forecast.departed ?  : null;

        let terminated = row.planned && row.planned.activity === 'TF' && service && service.destinationLocation && service.terminatedAt && service.destinationLocation.tiploc !== service.terminatedAt.tiploc,
            arrived = forecast.arrived && !forecast.departed,
            expected = !(forecast.arrived || forecast.departed);

        return

            {icon}

                <Location data={data} tiploc={row.tiploc}/>

                {row.cancelled ? 'Cancelled' : wtp ? 'Pass' : terminated ? "Terminated" : plat && (plat.suppressed && !forecast.departed) ? null : plat.plat}

                <Time time={forecast.time} terminated={terminated} arrived={arrived} expected={expected}/>

                <Delay delay={row.delay}/>

;

    }
}

export default Movement;
<|endoftext|>


<commit_before>/* SBHS-Timetable-Node: Countdown and timetable all at once (NodeJS app).
 * Copyright (C) 2014 James Ye, Simon Shields
 *
 * This file is part of SBHS-Timetable-Node.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/* globals belltimes, updateSidebarStatus, getNextSchoolDay, getLoggedIn */ /* jshint -W098 */

function handleTopPane() {
	'use strict';
	var entry, list, today = new Date(), res = '', j;
	if (!window.notices) {
		return;
	}
	if (window.noticesCached) {
		res += '
These notices might be old
';
	}
	var sorted = Object.keys(window.notices.notices).sort(function(a,b) {
		if (a >  b) {
			return -1;
		}
		return 1;
	});
	if (window.notices.date !== null) {
		res += '
Notices for ' + window.notices.date + ' (Week ' + window.notices.week + ')

';
	} else {
		res += '
Notices for ' + belltimes.date + ' (Week ' + belltimes.week + belltimes.weekType + ')

';
	}
	if (window.barcodenews) {
		res += '
';
		res += 'All Students and Staff	';
		res += '
Today\'s Barcode News

';
		res += '
';
		for (j in window.barcodenews.content.current) {
			list = window.barcodenews.content.current[j];
			res += '';
			if (list.years[0] !== 'all' && list.years.length > 1) {
				res += 'Years ' + list.years.join(', ');
			}
			else if (list.years[0] !== 'all' && list.years.length == 1) {
				res += 'Year ' + list.years[0];
			}
			else {
				res += 'Everyone';
			}
			res += '';
			res +=': ' + list.content + '
';
		}
		res += '

';
	}
	for (var i in sorted) {
		list = window.notices.notices[sorted[i]];
		for (j in list) {
			entry = list[j];
			res += '
';
			res += ''+entry.dTarget+'	';
			res += '
'+entry.title+'

';
			if (entry.isMeeting) {
				res += '
Meeting Date: ' + entry.meetingDate + '
';
				res += 'Meeting Time: ' + entry.meetingTime + ' in ' + entry.meetingPlace + '

';
			}
			res += '
';
			res += entry.text + '

'+entry.author+'

';
		}
	}
	res += '

';
	document.getElementById('top-pane').innerHTML = res;
	$('.notice-row').click(function() {
		/*jshint validthis: true*/
		var id = this.id;
		var el = $('#n'+id+'-hidden');
		if (!el.hasClass('velocity-animating')) {
			if (el.hasClass('notice-hidden')) {
				el.velocity('stop').removeClass('notice-hidden').velocity('slideDown');
			}
			else {
				el.velocity('stop').velocity('slideUp').addClass('notice-hidden');
			}
		}
	});

}

function handleNotices(err) {
	/*jshint validthis: true*/
	'use strict';
	window.noticesCached = false;
	var lsKey = new Date().toDateString();
	var res = JSON.parse(this.responseText);
	if (res.notices) {
		window.localStorage[lsKey] = this.responseText;
		window.notices = res;
		handleTopPane();
	}
	else if (!window.notices) {
		$('#top-pane').html('
Reload
');
	}
	updateSidebarStatus();
}

function handleBarcodeNews() {
	/*jshint validthis: true */
	'use strict';
	var res = JSON.parse(this.responseText);
	if (res.content) {
		window.barcodenews = res;
		handleTopPane();
	}
}

function loadBarcodeNews() {
	'use strict';
	var xhr;
	var ds = getDateString();

	if (getLoggedIn()) {
		xhr = new XMLHttpRequest();
		xhr.onload = handleBarcodeNews;
		xhr.open('GET', '/api/barcodenews.json?date='+ds, true);
		xhr.send();
	}
}

function loadNotices() {
	'use strict';
	window.noticesCached = false;
	var lsKey = new Date().toDateString();
	var ds = getDateString();
	console.log(ds);
	if (lsKey in window.localStorage) {
		window.noticesCached = true;
		window.notices = JSON.parse(window.localStorage[lsKey]);
		updateSidebarStatus();
		handleTopPane();
	}
	else if (!getLoggedIn()) {
		window.notices = { 'notices': { 'failure': true } };
		return;
	}
	if (!getLoggedIn()) {
		return;
	}
	var xhr = new XMLHttpRequest();
	xhr.onload = handleNotices;
	xhr.open('GET', '/api/notices.json?date='+ds, true);
	xhr.send();
	loadBarcodeNews();
}

<commit_msg>Add a notices filter that is actually obvious *ahem*<commit_after>/* SBHS-Timetable-Node: Countdown and timetable all at once (NodeJS app).
 * Copyright (C) 2014 James Ye, Simon Shields
 *
 * This file is part of SBHS-Timetable-Node.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/* globals belltimes, updateSidebarStatus, getNextSchoolDay, getLoggedIn */ /* jshint -W098 */

function handleTopPane() {
	'use strict';
	var entry, list, today = new Date(), res = '', j, i;
	if (!window.notices) {
		return;
	}
	if (window.noticesCached) {
		res += '
These notices might be old
';
	}
	var sorted = Object.keys(window.notices.notices).sort(function(a,b) {
		if (a >  b) {
			return -1;
		}
		return 1;
	});
	res += '<select id="notices-filter">';
	res += '<option value="notice">All notices</option>';
	for (i = 7; i <= 12; i++) {
		res += '<option value="notice'+i+'">Year ' + i + '</option>';
	}
	res += '<option value="noticeStaff">Staff</option></select>';
	if (window.notices.date !== null) {
		res += '
Notices for ' + window.notices.date + ' (Week ' + window.notices.week + ')

';
	} else {
		res += '
Notices for ' + belltimes.date + ' (Week ' + belltimes.week + belltimes.weekType + ')

';
	}
	if (window.barcodenews) {
		res += '
';
		res += 'All Students and Staff	';
		res += '
Today\'s Barcode News

';
		res += '
';
		for (j in window.barcodenews.content.current) {
			list = window.barcodenews.content.current[j];
			res += '';
			if (list.years[0] !== 'all' && list.years.length > 1) {
				res += 'Years ' + list.years.join(', ');
			}
			else if (list.years[0] !== 'all' && list.years.length == 1) {
				res += 'Year ' + list.years[0];
			}
			else {
				res += 'Everyone';
			}
			res += '';
			res +=': ' + list.content + '
';
		}
		res += '

';
	}
	for (i in sorted) {
		list = window.notices.notices[sorted[i]];
		for (j in list) {
			entry = list[j];
			res += '
';
			res += ''+entry.dTarget+'	';
			res += '
'+entry.title+'

';
			if (entry.isMeeting) {
				res += '
Meeting Date: ' + entry.meetingDate + '
';
				res += 'Meeting Time: ' + entry.meetingTime + ' in ' + entry.meetingPlace + '

';
			}
			res += '
';
			res += entry.text + '

'+entry.author+'

';
		}
	}
	res += '

';
	document.getElementById('top-pane').innerHTML = res;
	$('.notice-row').click(function() {
		/*jshint validthis: true*/
		var id = this.id;
		var el = $('#n'+id+'-hidden');
		if (!el.hasClass('velocity-animating')) {
			if (el.hasClass('notice-hidden')) {
				el.velocity('stop').removeClass('notice-hidden').velocity('slideDown');
			}
			else {
				el.velocity('stop').velocity('slideUp').addClass('notice-hidden');
			}
		}
	});

	$('#notices-filter').change(function() {
		/*jshint validthis: true*/
		console.log('change!');
		var val = this.value;
		$('.notice').velocity('fadeOut');
		$('.'+val).velocity('stop').velocity('fadeIn');
	});

}

function handleNotices(err) {
	/*jshint validthis: true*/
	'use strict';
	window.noticesCached = false;
	var lsKey = new Date().toDateString();
	var res = JSON.parse(this.responseText);
	if (res.notices) {
		window.localStorage[lsKey] = this.responseText;
		window.notices = res;
		handleTopPane();
	}
	else if (!window.notices) {
		$('#top-pane').html('
Reload
');
	}
	updateSidebarStatus();
}

function handleBarcodeNews() {
	/*jshint validthis: true */
	'use strict';
	var res = JSON.parse(this.responseText);
	if (res.content) {
		window.barcodenews = res;
		handleTopPane();
	}
}

function loadBarcodeNews() {
	'use strict';
	var xhr;
	var ds = getDateString();

	if (getLoggedIn()) {
		xhr = new XMLHttpRequest();
		xhr.onload = handleBarcodeNews;
		xhr.open('GET', '/api/barcodenews.json?date='+ds, true);
		xhr.send();
	}
}

function loadNotices() {
	'use strict';
	window.noticesCached = false;
	var lsKey = new Date().toDateString();
	var ds = getDateString();
	console.log(ds);
	if (lsKey in window.localStorage) {
		window.noticesCached = true;
		window.notices = JSON.parse(window.localStorage[lsKey]);
		updateSidebarStatus();
		handleTopPane();
	}
	else if (!getLoggedIn()) {
		window.notices = { 'notices': { 'failure': true } };
		return;
	}
	if (!getLoggedIn()) {
		return;
	}
	var xhr = new XMLHttpRequest();
	xhr.onload = handleNotices;
	xhr.open('GET', '/api/notices.json?date='+ds, true);
	xhr.send();
	loadBarcodeNews();
}

<|endoftext|>


<commit_before>
"use strict";

import Model from '../classes/Model';
import Collection from '../classes/Collection';

let http = undefined;
let cache = undefined;

function appendTransform(transform) {
    let defaults = http.defaults.transformResponse;
    defaults = angular.isArray(defaults) ? defaults : [defaults];
    return defaults.concat(transform);
};

/**
 * Default base Manager to extend upon.
 */
export default class Manager {

    /**
     * Class constructor.
     *
     * @param object $http Injected $http service.
     * @param object $cacheFactory Injector $cacheFactory service.
     * @return void
     */
    constructor($http, $cacheFactory) {
        http = $http;
        if (cache === undefined) {
            cache = $cacheFactory('monad-managers');
        }
        /**
         * The current Model class to work on. You may set this per derived
         * manager where needed. Defaults to base Monad Model.
         */
        this.model = Model;
        /**
         * The current Collection object to work on. You may set this per
         * derived manager where needed. Defaults to base Monad Collection.
         */
        this.collection = new Collection();
        /**
         * Bindable pointer for specified list filters, if any.
         */
        this.filter = {};
    }

    /**
     * Apply the currently set filter to params.
     *
     * @param object params Hash of params to augment.
     * @return object Augmented params.
     */
    applyFilter(params) {
        if (this.filter) {
            for (let p in this.filter) {
                if (this.filter[p] !== false) {
                    params[p] = this.filter[p];
                }
            }
        }
        return params;
    }

    /**
     * Virtual getter for the cache object.
     */
    get cache() {
        return cache;
    }

    /**
     * Virtual count of total current list items. Here it returns 0; your custom
     * Manager _may_ offer an actual implementation of this (it depends on the
     * API you're using).
     *
     * @return integer Number of total items in the current list.
     */
    get count() {
        return 0;
    }

    /**
     * Retrieve a list (Collection) of items.
     *
     * @param string url The API URL to query.
     * @return Promise An Angular promise yielding a Collection on `success`.
     */
    list(url) {
        return http({
            url,
            method: 'GET',
            transformResponse: appendTransform(values => {
                while (this.collection.length) {
                    this.collection.pop();
                }
                values.map(value => this.collection.push((new this.model()).$load(value)));
                return this.collection;
            }),
            cache
        });
    }

    /**
     * Find a single item.
     *
     * @param string url The API URL to query.
     * @return Primse An Angular promise yielding a Model on `success`.
     */
    find(url) {
        return http({
            url,
            method: 'GET',
            transformResponse: appendTransform(item => (new this.model()).$load(item)),
            cache
        });
    }

    /**
     * Save the specified model back to the API.
     *
     * @param object model The model to save.
     * @return mixed On success, a Promise yielding the API's return value on
     *  `success`. On failure (because e.g. model is not dirty) an empty object.
     */
    save(model) {
        if (model.$new) {
            return this.create(model).success(() => model.$initial = model.$data);
        } else if (model.$deleted) {
            return this['delete'](model).success(() => {
                model.$data = {};
                model.$initial = undefined;
            });
        } else if (model.$dirty) {
            return this.update(model).success(() => model.$initial = model.$data);
        }
        return {};
    }

    /**
     * API interface. These should be overridden by a custom implementation,
     * since we have no way to guesstimate how your API works. Hence, these
     * throw an error as a friendly reminder :)
     *
     * The actual implementations should return promises.
     *
     * @param object model The model to create.
     */
    create(model) {
        throw "Manager.create must use a custom implementation.";
    }

    /**
     * API interface. These should be overridden by a custom implementation,
     * since we have no way to guesstimate how your API works. Hence, these
     * throw an error as a friendly reminder :)
     *
     * The actual implementations should return promises.
     *
     * @param object model The model to update.
     */
    update(model) {
        throw "Manager.update must use a custom implementation.";
    }

    /**
     * API interface. These should be overridden by a custom implementation,
     * since we have no way to guesstimate how your API works. Hence, these
     * throw an error as a friendly reminder :)
     *
     * The actual implementations should return promises.
     *
     * @param object model The model to delete.
     */
    ['delete'](model) {
        throw "Manager.delete must use a custom implementation.";
    }

    /**
     * Virtual property to access the $http service.
     *
     * @return object Angular $http service.
     */
    get http() {
        return http;
    }

};

Manager.$inject = ['$http', '$cacheFactory'];

<commit_msg>use static properties for Model and Collection, it is cleaner<commit_after>
"use strict";

import Model from '../classes/Model';
import Collection from '../classes/Collection';

let http = undefined;
let cache = undefined;

function appendTransform(transform) {
    let defaults = http.defaults.transformResponse;
    defaults = angular.isArray(defaults) ? defaults : [defaults];
    return defaults.concat(transform);
};

/**
 * Default base Manager to extend upon.
 */
export default class Manager {

    /**
     * Class constructor.
     *
     * @param object $http Injected $http service.
     * @param object $cacheFactory Injector $cacheFactory service.
     * @return void
     */
    constructor($http, $cacheFactory) {
        http = $http;
        if (cache === undefined) {
            cache = $cacheFactory('monad-managers');
        }
        /**
         * Bindable pointer for specified list filters, if any.
         */
        this.filter = {};
    }

    /**
     * Apply the currently set filter to params.
     *
     * @param object params Hash of params to augment.
     * @return object Augmented params.
     */
    applyFilter(params) {
        if (this.filter) {
            for (let p in this.filter) {
                if (this.filter[p] !== false) {
                    params[p] = this.filter[p];
                }
            }
        }
        return params;
    }

    /**
     * Virtual getter for the cache object.
     */
    get cache() {
        return cache;
    }

    /**
     * Virtual count of total current list items. Here it returns 0; your custom
     * Manager _may_ offer an actual implementation of this (it depends on the
     * API you're using).
     *
     * @return integer Number of total items in the current list.
     */
    get count() {
        return 0;
    }

    /**
     * Retrieve a list (Collection) of items.
     *
     * @param string url The API URL to query.
     * @return Promise An Angular promise yielding a Collection on `success`.
     */
    list(url) {
        return http({
            url,
            method: 'GET',
            transformResponse: appendTransform(values => {
                while (this.constructor.Collection.length) {
                    this.constructor.Collection.pop();
                }
                values.map(value => this.constructor.Collection.push((new this.constructor.Model()).$load(value)));
                return this.constructor.Collection;
            }),
            cache
        });
    }

    /**
     * Find a single item.
     *
     * @param string url The API URL to query.
     * @return Primse An Angular promise yielding a Model on `success`.
     */
    find(url) {
        return http({
            url,
            method: 'GET',
            transformResponse: appendTransform(item => (new this.constructor.Model()).$load(item)),
            cache
        });
    }

    /**
     * Save the specified model back to the API.
     *
     * @param object model The model to save.
     * @return mixed On success, a Promise yielding the API's return value on
     *  `success`. On failure (because e.g. model is not dirty) an empty object.
     */
    save(model) {
        if (model.$new) {
            return this.create(model).success(() => model.$initial = model.$data);
        } else if (model.$deleted) {
            return this['delete'](model).success(() => {
                model.$data = {};
                model.$initial = undefined;
            });
        } else if (model.$dirty) {
            return this.update(model).success(() => model.$initial = model.$data);
        }
        return {};
    }

    /**
     * API interface. These should be overridden by a custom implementation,
     * since we have no way to guesstimate how your API works. Hence, these
     * throw an error as a friendly reminder :)
     *
     * The actual implementations should return promises.
     *
     * @param object model The model to create.
     */
    create(model) {
        throw "Manager.create must use a custom implementation.";
    }

    /**
     * API interface. These should be overridden by a custom implementation,
     * since we have no way to guesstimate how your API works. Hence, these
     * throw an error as a friendly reminder :)
     *
     * The actual implementations should return promises.
     *
     * @param object model The model to update.
     */
    update(model) {
        throw "Manager.update must use a custom implementation.";
    }

    /**
     * API interface. These should be overridden by a custom implementation,
     * since we have no way to guesstimate how your API works. Hence, these
     * throw an error as a friendly reminder :)
     *
     * The actual implementations should return promises.
     *
     * @param object model The model to delete.
     */
    ['delete'](model) {
        throw "Manager.delete must use a custom implementation.";
    }

    /**
     * Virtual property to access the $http service.
     *
     * @return object Angular $http service.
     */
    get http() {
        return http;
    }

};

Manager.$inject = ['$http', '$cacheFactory'];
/**
 * The current Model class to work on. You may set this per derived
 * manager where needed. Defaults to base Monad Model.
 */
Manager.Model = Model;
/**
 * The current Collection object to work on. You may set this per
 * derived manager where needed. Defaults to base Monad Collection.
 */
Manager.Collection = new Collection();

<|endoftext|>


<commit_before>
// FIXME: type template (using custom builder)
angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(sfPathProvider) {

  var SNAKE_CASE_REGEXP = /[A-Z]/g;
  var snakeCase = function(name, separator) {
    separator = separator || '_';
    return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
      return (pos ? separator : '') + letter.toLowerCase();
    });
  };
  var formId = 0;

  var builders = {
    sfField: function(args) {
      args.fieldFrag.firstChild.setAttribute('sf-field', formId);

      // We use a lookup table for easy access to our form.
      args.lookup['f' + formId] = args.form;
      formId++;
    },
    ngModel: function(args) {
      if (!args.form.key) {
        return;
      }
      var key  = args.form.key;

      // Redact part of the key, used in arrays
      // KISS keyRedaction is a number.
      if (args.state.keyRedaction) {
        key = key.slice(args.state.keyRedaction);
      }

      // Stringify key.
      var modelValue;
      if (!args.state.modelValue) {
        var strKey = sfPathProvider.stringify(key).replace(/"/g, '"');
        modelValue = (args.state.modelName || 'model');

        if (strKey) { // Sometimes, like with arrays directly in arrays strKey is nothing.
          modelValue += (strKey[0] !== '[' ? '.' : '') + strKey;
        }
      } else {
        // Another builder, i.e. array has overriden the modelValue
        modelValue = args.state.modelValue;
      }

      // Find all sf-field-value attributes.
      // No value means a add a ng-model.
      // sf-field-value="replaceAll", loop over attributes and replace $$value$$ in each.
      // sf-field-value="attrName", replace or set value of that attribute.
      var nodes = args.fieldFrag.querySelectorAll('[sf-field-model]');
      for (var i = 0; i < nodes.length; i++) {
        var n = nodes[i];
        var conf = n.getAttribute('sf-field-model');
        if (!conf || conf === '') {
          n.setAttribute('ng-model', modelValue);
        } else if (conf === 'replaceAll') {
          var attributes = n.attributes;
          for (var j = 0; j < attributes.length; j++) {
            if (attributes[j].value && attributes[j].value.indexOf('$$value') !== -1) {
              attributes[j].value = attributes[j].value.replace(/\$\$value\$\$/g, modelValue);
            }
          }
        } else {
          var  val = n.getAttribute(conf);
          if (val && val.indexOf('$$value$$')) {
            n.setAttribute(conf, val.replace(/\$\$value\$\$/g, modelValue));
          } else {
            n.setAttribute(conf, modelValue);
          }
        }
      }
    },
    simpleTransclusion: function(args) {
      var children = args.build(args.form.items, args.path + '.items', args.state);
      args.fieldFrag.firstChild.appendChild(children);
    },

    // Patch on ngModelOptions, since it doesn't like waiting for its value.
    ngModelOptions: function(args) {
      if (args.form.ngModelOptions && Object.keys(args.form.ngModelOptions).length > 0) {
        args.fieldFrag.firstChild.setAttribute('ng-model-options', JSON.stringify(args.form.ngModelOptions));
      }
    },
    transclusion: function(args) {
      var transclusions = args.fieldFrag.querySelectorAll('[sf-field-transclude]');

      if (transclusions.length) {
        for (var i = 0; i < transclusions.length; i++) {
          var n = transclusions[i];

          // The sf-transclude attribute is not a directive, but has the name of what we're supposed to
          // traverse.
          var sub = args.form[n.getAttribute('sf-field-transclude')];
          if (sub) {
            sub = Array.isArray(sub) ? sub : [sub];
            var childFrag = args.build(sub, args.path + '.' + sub, args.state);
            n.appendChild(childFrag);
          }
        }
      }
    },
    condition: function(args) {
      // Do we have a condition? Then we slap on an ng-if on all children,
      // but be nice to existing ng-if.
      if (args.form.condition) {
        var evalExpr = 'evalExpr(' + args.path +
                       '.contidion, { model: model, "arrayIndex": $index})';
        if (args.form.key) {
          var strKey = sfPathProvider.stringify(args.form.key);
          evalExpr = 'evalExpr(' + args.path + '.condition,{ model: model, "arrayIndex": $index, ' +
                     '"modelValue": model' + (strKey[0] === '[' ? '' : '.') + strKey + '})';
        }

        var children = args.fieldFrag.children;
        for (var i = 0; i < children.length; i++) {
          var child = children[i];
          var ngIf = child.getAttribute('ng-if');
          child.setAttribute(
            'ng-if',
            ngIf ?
            '(' + ngIf +
            ') || (' + evalExpr + ')'
            : evalExpr
          );
        }
      }
    },
    array: function(args) {
      var items = args.fieldFrag.querySelector('[schema-form-array-items]');
      if (items) {
        state = angular.copy(args.state);
        state.keyRedaction = state.keyRedaction || 0;
        state.keyRedaction += args.form.key.length + 1;

        // Special case, an array with just one item in it that is not an object.
        // So then we just override the modelValue
        if (args.form.schema && args.form.schema.items &&
            args.form.schema.items.type &&
            args.form.schema.items.type.indexOf('object') === -1 &&
            args.form.schema.items.type.indexOf('array') === -1) {
          var strKey = sfPathProvider.stringify(args.form.key).replace(/"/g, '"') + '[$index]';
          state.modelValue = 'modelArray[$index]';
        } else {
          state.modelName = 'item';
        }

        // Flag to the builder that where in an array.
        // This is needed for compatabiliy if a "old" add-on is used that
        // hasn't been transitioned to the new builder.
        state.arrayCompatFlag = true;

        var childFrag = args.build(args.form.items, args.path + '.items', state);
        items.appendChild(childFrag);
      }
    }
  };
  this.builders = builders;

  this.$get = ['$templateCache', 'schemaFormDecorators', 'sfPath', function($templateCache, schemaFormDecorators, sfPath) {

    var checkForSlot = function(form, slots) {
      // Finally append this field to the frag.
      // Check for slots
      if (form.key) {
        var slot = slots[sfPath.stringify(form.key)];
        if (slot) {
          while (slot.firstChild) {
            slot.removeChild(slot.firstChild);
          }
          return slot;
        }
      }
    };

    var build = function(items, decorator, templateFn, slots, path, state, lookup) {
      state = state || {};
      lookup = lookup || Object.create(null);
      path = path || 'schemaForm.form';
      var container = document.createDocumentFragment();
      items.reduce(function(frag, f, index) {

        // Sanity check.
        if (!f.type) {
          return;
        }

        var field = decorator[f.type] || decorator['default'];
        if (!field.replace) {
          // Backwards compatability build
          var n = document.createElement(snakeCase(decorator.__name, '-'));
          if (state.arrayCompatFlag) {
            n.setAttribute('form','copyWithIndex($index)');
          } else {
            n.setAttribute('form', path + '[' + index + ']');
          }

          (checkForSlot(f, slots) || frag).appendChild(n);

        } else {
          var tmpl;

          // Reset arrayCompatFlag, it's only valid for direct children of the array.
          state.arrayCompatFlag = false;

          // TODO: Create a couple fo testcases, small and large and
          //       measure optmization. A good start is probably a cache of DOM nodes for a particular
          //       template that can be cloned instead of using innerHTML
          var div = document.createElement('div');
          var template = templateFn(field.template) || templateFn([decorator['default'].template]);
          div.innerHTML = template;

          // Move node to a document fragment, we don't want the div.
          tmpl = document.createDocumentFragment();
          while (div.childNodes.length > 0) {
            tmpl.appendChild(div.childNodes[0]);
          }

          // Possible builder, often a noop
          var args = {
            fieldFrag: tmpl,
            form: f,
            lookup: lookup,
            state: state,
            path: path + '[' + index + ']',

            // Recursive build fn
            build: function(items, path, state) {
              return build(items, decorator, templateFn, slots, path, state, lookup);
            },

          };

          // Builders are either a function or a list of functions.
          if (typeof field.builder === 'function') {
            field.builder(args);
          } else {
            field.builder.forEach(function(fn) { fn(args); });
          }

          // Append
          (checkForSlot(f, slots) || frag).appendChild(tmpl);
        }
        return frag;
      }, container);

      return container;
    };

    return {
      /**
       * Builds a form from a canonical form definition
       */
      build: function(form, decorator, slots, lookup) {
        return build(form, decorator, function(url) {
          return $templateCache.get(url);
        }, slots, undefined, undefined, lookup);

      },
      builder: builders,
      internalBuild: build
    };
  }];

}]);
<commit_msg>Bugfix, don't rely on documentFragment.children<commit_after>
// FIXME: type template (using custom builder)
angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(sfPathProvider) {

  var SNAKE_CASE_REGEXP = /[A-Z]/g;
  var snakeCase = function(name, separator) {
    separator = separator || '_';
    return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
      return (pos ? separator : '') + letter.toLowerCase();
    });
  };
  var formId = 0;

  var builders = {
    sfField: function(args) {
      args.fieldFrag.firstChild.setAttribute('sf-field', formId);

      // We use a lookup table for easy access to our form.
      args.lookup['f' + formId] = args.form;
      formId++;
    },
    ngModel: function(args) {
      if (!args.form.key) {
        return;
      }
      var key  = args.form.key;

      // Redact part of the key, used in arrays
      // KISS keyRedaction is a number.
      if (args.state.keyRedaction) {
        key = key.slice(args.state.keyRedaction);
      }

      // Stringify key.
      var modelValue;
      if (!args.state.modelValue) {
        var strKey = sfPathProvider.stringify(key).replace(/"/g, '"');
        modelValue = (args.state.modelName || 'model');

        if (strKey) { // Sometimes, like with arrays directly in arrays strKey is nothing.
          modelValue += (strKey[0] !== '[' ? '.' : '') + strKey;
        }
      } else {
        // Another builder, i.e. array has overriden the modelValue
        modelValue = args.state.modelValue;
      }

      // Find all sf-field-value attributes.
      // No value means a add a ng-model.
      // sf-field-value="replaceAll", loop over attributes and replace $$value$$ in each.
      // sf-field-value="attrName", replace or set value of that attribute.
      var nodes = args.fieldFrag.querySelectorAll('[sf-field-model]');
      for (var i = 0; i < nodes.length; i++) {
        var n = nodes[i];
        var conf = n.getAttribute('sf-field-model');
        if (!conf || conf === '') {
          n.setAttribute('ng-model', modelValue);
        } else if (conf === 'replaceAll') {
          var attributes = n.attributes;
          for (var j = 0; j < attributes.length; j++) {
            if (attributes[j].value && attributes[j].value.indexOf('$$value') !== -1) {
              attributes[j].value = attributes[j].value.replace(/\$\$value\$\$/g, modelValue);
            }
          }
        } else {
          var  val = n.getAttribute(conf);
          if (val && val.indexOf('$$value$$')) {
            n.setAttribute(conf, val.replace(/\$\$value\$\$/g, modelValue));
          } else {
            n.setAttribute(conf, modelValue);
          }
        }
      }
    },
    simpleTransclusion: function(args) {
      var children = args.build(args.form.items, args.path + '.items', args.state);
      args.fieldFrag.firstChild.appendChild(children);
    },

    // Patch on ngModelOptions, since it doesn't like waiting for its value.
    ngModelOptions: function(args) {
      if (args.form.ngModelOptions && Object.keys(args.form.ngModelOptions).length > 0) {
        args.fieldFrag.firstChild.setAttribute('ng-model-options', JSON.stringify(args.form.ngModelOptions));
      }
    },
    transclusion: function(args) {
      var transclusions = args.fieldFrag.querySelectorAll('[sf-field-transclude]');

      if (transclusions.length) {
        for (var i = 0; i < transclusions.length; i++) {
          var n = transclusions[i];

          // The sf-transclude attribute is not a directive, but has the name of what we're supposed to
          // traverse.
          var sub = args.form[n.getAttribute('sf-field-transclude')];
          if (sub) {
            sub = Array.isArray(sub) ? sub : [sub];
            var childFrag = args.build(sub, args.path + '.' + sub, args.state);
            n.appendChild(childFrag);
          }
        }
      }
    },
    condition: function(args) {
      // Do we have a condition? Then we slap on an ng-if on all children,
      // but be nice to existing ng-if.
      if (args.form.condition) {
        var evalExpr = 'evalExpr(' + args.path +
                       '.contidion, { model: model, "arrayIndex": $index})';
        if (args.form.key) {
          var strKey = sfPathProvider.stringify(args.form.key);
          evalExpr = 'evalExpr(' + args.path + '.condition,{ model: model, "arrayIndex": $index, ' +
                     '"modelValue": model' + (strKey[0] === '[' ? '' : '.') + strKey + '})';
        }

        var children = args.fieldFrag.children || args.fieldFrag.childNodes;
        for (var i = 0; i < children.length; i++) {
          var child = children[i];
          var ngIf = child.getAttribute('ng-if');
          child.setAttribute(
            'ng-if',
            ngIf ?
            '(' + ngIf +
            ') || (' + evalExpr + ')'
            : evalExpr
          );
        }
      }
    },
    array: function(args) {
      var items = args.fieldFrag.querySelector('[schema-form-array-items]');
      if (items) {
        state = angular.copy(args.state);
        state.keyRedaction = state.keyRedaction || 0;
        state.keyRedaction += args.form.key.length + 1;

        // Special case, an array with just one item in it that is not an object.
        // So then we just override the modelValue
        if (args.form.schema && args.form.schema.items &&
            args.form.schema.items.type &&
            args.form.schema.items.type.indexOf('object') === -1 &&
            args.form.schema.items.type.indexOf('array') === -1) {
          var strKey = sfPathProvider.stringify(args.form.key).replace(/"/g, '"') + '[$index]';
          state.modelValue = 'modelArray[$index]';
        } else {
          state.modelName = 'item';
        }

        // Flag to the builder that where in an array.
        // This is needed for compatabiliy if a "old" add-on is used that
        // hasn't been transitioned to the new builder.
        state.arrayCompatFlag = true;

        var childFrag = args.build(args.form.items, args.path + '.items', state);
        items.appendChild(childFrag);
      }
    }
  };
  this.builders = builders;

  this.$get = ['$templateCache', 'schemaFormDecorators', 'sfPath', function($templateCache, schemaFormDecorators, sfPath) {

    var checkForSlot = function(form, slots) {
      // Finally append this field to the frag.
      // Check for slots
      if (form.key) {
        var slot = slots[sfPath.stringify(form.key)];
        if (slot) {
          while (slot.firstChild) {
            slot.removeChild(slot.firstChild);
          }
          return slot;
        }
      }
    };

    var build = function(items, decorator, templateFn, slots, path, state, lookup) {
      state = state || {};
      lookup = lookup || Object.create(null);
      path = path || 'schemaForm.form';
      var container = document.createDocumentFragment();
      items.reduce(function(frag, f, index) {

        // Sanity check.
        if (!f.type) {
          return;
        }

        var field = decorator[f.type] || decorator['default'];
        if (!field.replace) {
          // Backwards compatability build
          var n = document.createElement(snakeCase(decorator.__name, '-'));
          if (state.arrayCompatFlag) {
            n.setAttribute('form','copyWithIndex($index)');
          } else {
            n.setAttribute('form', path + '[' + index + ']');
          }

          (checkForSlot(f, slots) || frag).appendChild(n);

        } else {
          var tmpl;

          // Reset arrayCompatFlag, it's only valid for direct children of the array.
          state.arrayCompatFlag = false;

          // TODO: Create a couple fo testcases, small and large and
          //       measure optmization. A good start is probably a cache of DOM nodes for a particular
          //       template that can be cloned instead of using innerHTML
          var div = document.createElement('div');
          var template = templateFn(field.template) || templateFn([decorator['default'].template]);
          div.innerHTML = template;

          // Move node to a document fragment, we don't want the div.
          tmpl = document.createDocumentFragment();
          while (div.childNodes.length > 0) {
            tmpl.appendChild(div.childNodes[0]);
          }

          // Possible builder, often a noop
          var args = {
            fieldFrag: tmpl,
            form: f,
            lookup: lookup,
            state: state,
            path: path + '[' + index + ']',

            // Recursive build fn
            build: function(items, path, state) {
              return build(items, decorator, templateFn, slots, path, state, lookup);
            },

          };

          // Builders are either a function or a list of functions.
          if (typeof field.builder === 'function') {
            field.builder(args);
          } else {
            field.builder.forEach(function(fn) { fn(args); });
          }

          // Append
          (checkForSlot(f, slots) || frag).appendChild(tmpl);
        }
        return frag;
      }, container);

      return container;
    };

    return {
      /**
       * Builds a form from a canonical form definition
       */
      build: function(form, decorator, slots, lookup) {
        return build(form, decorator, function(url) {
          return $templateCache.get(url);
        }, slots, undefined, undefined, lookup);

      },
      builder: builders,
      internalBuild: build
    };
  }];

}]);
<|endoftext|>


<commit_before>requirejs.config({
    paths: {
        jquery: '../bower_components/jquery/dist/jquery',
        knockout: '../bower_components/knockout/dist/knockout',
        leaflet: '../bower_components/leaflet/dist/leaflet',
        mout: '../bower_components/mout/src'
    }
});

requirejs(['leaflet', 'view-models/MapControls', 'knockout'], function(L, MapControls, ko) {
    var map = L.map('map').setView([44.873571, -91.299130], 16);

    L.tileLayer('https://{s}.tiles.mapbox.com/v3/examples.map-i87786ca/{z}/{x}/{y}.png', {
        attribution: 'Terms & Feedback'
    }).addTo(map);

    var mapControls = new MapControls(map, {
        trees: [
            {
                'id': 188,
                'type': 'Gala',
                'latitude': 44.873571,
                'longitude': -91.299130
            }
        ]
    });

    map.addControl(mapControls);
    ko.applyBindings(mapControls);
});
<commit_msg>Update config.js<commit_after>requirejs.config({
    paths: {
        jquery: '../bower_components/jquery/dist/jquery',
        knockout: '../bower_components/knockout/dist/knockout',
        leaflet: '../bower_components/leaflet/dist/leaflet',
        mout: '../bower_components/mout/src'
    }
});

requirejs(['leaflet', 'view-models/MapControls', 'knockout'], function(L, MapControls, ko) {
    var map = L.map('map').setView([44.873571, -91.299130], 16);

    L.tileLayer('https://{s}.tiles.mapbox.com/v3/examples.map-i87786ca/{z}/{x}/{y}.png', {
        attribution: 'Terms & Feedback'
    }).addTo(map);

    var mapControls = new MapControls(map, {
        trees: [
            {
                'id': 188,
                'type': 'Gala',
                'latitude': 44.873571,
                'longitude': -91.299130
            }
        ]
    });

    map.addControl(mapControls);
    ko.applyBindings(mapControls, $('#controls-container').get(0));
});
<|endoftext|>


<commit_before>// Handles the page publication date fields
export default function () {
  document.addEventListener("DialogReady.Alchemy", function (evt) {
    const dialog = evt.detail.body
    const public_on_field = dialog.querySelector("#page_public_on")
    const public_until_field = dialog.querySelector("#page_public_until")
    const publication_date_fields = dialog.querySelector(
      ".page-publication-date-fields"
    )

    dialog
      .querySelector("#page_public")
      .addEventListener("click", function (evt) {
        const checkbox = evt.target
        const now = new Date()

        if (checkbox.checked) {
          publication_date_fields.classList.remove("hidden")
          public_on_field._flatpickr.setDate(now)
        } else {
          publication_date_fields.classList.add("hidden")
          public_on_field.value = ""
        }
        public_until_field.value = ""
      })
  })
}
<commit_msg>check presence of page_public checkbox<commit_after>// Handles the page publication date fields
export default function () {
  document.addEventListener("DialogReady.Alchemy", function (evt) {
    const dialog = evt.detail.body
    const public_on_field = dialog.querySelector("#page_public_on")
    const public_until_field = dialog.querySelector("#page_public_until")
    const publication_date_fields = dialog.querySelector(
      ".page-publication-date-fields"
    )
    const public_field = dialog.querySelector("#page_public")

    if(!public_field) return

    public_field.addEventListener("click", function (evt) {
      const checkbox = evt.target
      const now = new Date()

      if (checkbox.checked) {
        publication_date_fields.classList.remove("hidden")
        public_on_field._flatpickr.setDate(now)
      } else {
        publication_date_fields.classList.add("hidden")
        public_on_field.value = ""
      }
      public_until_field.value = ""
    })
  })
}
<|endoftext|>


<commit_before>/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/
var fs = require('fs'),
    path = require('path'),
    shell = require('shelljs'),
    util = require('../util'),
    events = require('../events'),
    Q = require('q'),
    ConfigParser = require('../ConfigParser');

module.exports = function firefoxos_parser(project) {
    this.path = project;
};

// Returns a promise.
module.exports.check_requirements = function(project_root) {
    return Q(); // Requirements always met.
};

module.exports.prototype = {
    // Returns a promise.
    update_from_config: function() {
        var config = new ConfigParser(this.config_xml());
        var manifestPath = path.join(this.www_dir(), 'manifest.webapp');
        var manifest;

        if(fs.existsSync(manifestPath)) {
            manifest = JSON.parse(fs.readFileSync(manifestPath));
        }
        else {
            manifest = {
                launch_path: "/index.html",
                installs_allowed_from: ["*"]
            };
        }

        manifest.version = config.version();
        manifest.name = config.name();
        manifest.pkgName = config.packageName();
        manifest.description = config.description();
        manifest.developer = {
            name: config.author()
        };

        var authorNode = config.doc.find('author');
        var authorUrl = authorNode.attrib['href'];

        if (authorUrl) {
            manifest.developer.url = authorUrl;
        }

        var permissionNodes = config.doc.findall('permission');

        if (permissionNodes.length) {
            manifest.permissions = manifest.permissions || {};

            permissionNodes.forEach(function(node) {
                var permissionName = node.attrib['name'];

                // Don't change if it was already created
                if (!manifest.permissions[permissionName]) {
                    manifest.permissions[permissionName] = {
                        description: node.attrib['description']
                    };

                    if (node.attrib['access']) {
                        manifest.permissions[permissionName].access = node.attrib['access'];
                    }
                }
            });
        }

        fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 4));

        return Q();
    },

    www_dir: function() {
        return path.join(this.path, 'www');
    },

    // Used for creating platform_www in projects created by older versions.
    cordovajs_path:function(libDir) {
        var jsPath = path.join(libDir, 'cordova-lib', 'cordova.js');
        return path.resolve(jsPath);
    },

    // Replace the www dir with contents of platform_www and app www.
    update_www:function() {
        var projectRoot = util.isCordova(this.path);
        var app_www = util.projectWww(projectRoot);
        var platform_www = path.join(this.path, 'platform_www');

        // Clear the www dir
        shell.rm('-rf', this.www_dir());
        shell.mkdir(this.www_dir());
        // Copy over all app www assets
        shell.cp('-rf', path.join(app_www, '*'), this.www_dir());
        // Copy over stock platform www assets (cordova.js)
        shell.cp('-rf', path.join(platform_www, '*'), this.www_dir());
    },

    update_overrides: function() {
        var projectRoot = util.isCordova(this.path);
        var mergesPath = path.join(util.appDir(projectRoot), 'merges', 'firefoxos');
        if(fs.existsSync(mergesPath)) {
            var overrides = path.join(mergesPath, '*');
            shell.cp('-rf', overrides, this.www_dir());
        }
    },

    config_xml:function(){
        return path.join(this.path, 'config.xml');
    },

    // Returns a promise.
    update_project: function(cfg) {
        return this.update_from_config()
            .then(function(){
                this.update_overrides();
                util.deleteSvnFolders(this.www_dir());
            }.bind(this));
    }
};
<commit_msg>Adding support for privileged<commit_after>/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/
var fs = require('fs'),
    path = require('path'),
    shell = require('shelljs'),
    util = require('../util'),
    events = require('../events'),
    Q = require('q'),
    ConfigParser = require('../ConfigParser');

module.exports = function firefoxos_parser(project) {
    this.path = project;
};

// Returns a promise.
module.exports.check_requirements = function(project_root) {
    return Q(); // Requirements always met.
};

module.exports.prototype = {
    // Returns a promise.
    update_from_config: function() {
        var config = new ConfigParser(this.config_xml());
        var manifestPath = path.join(this.www_dir(), 'manifest.webapp');
        var manifest;

        if(fs.existsSync(manifestPath)) {
            manifest = JSON.parse(fs.readFileSync(manifestPath));
        }
        else {
            manifest = {
                launch_path: "/index.html",
                installs_allowed_from: ["*"]
            };
        }

        manifest.version = config.version();
        manifest.name = config.name();
        manifest.pkgName = config.packageName();
        manifest.description = config.description();
        manifest.developer = {
            name: config.author()
        };

        var authorNode = config.doc.find('author');
        var authorUrl = authorNode.attrib['href'];

        if (authorUrl) {
            manifest.developer.url = authorUrl;
        }

        var permissionNodes = config.doc.findall('permission');
        var privileged = false;

        if (permissionNodes.length) {
            manifest.permissions = manifest.permissions || {};

            permissionNodes.forEach(function(node) {
                var permissionName = node.attrib['name'];

                // Don't change if it was already created
                if (!manifest.permissions[permissionName]) {
                    manifest.permissions[permissionName] = {
                        description: node.attrib['description']
                    };

                    if (node.attrib['access']) {
                        manifest.permissions[permissionName].access = node.attrib['access'];
                    }

                    if (node.attrib['privileged']) {
                        privileged = true;
                    }
                }
            });
        }

        if (privileged) {
            manifest.type = 'privileged';
        } else {
            delete manifest.type;
        }

        fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 4));

        return Q();
    },

    www_dir: function() {
        return path.join(this.path, 'www');
    },

    // Used for creating platform_www in projects created by older versions.
    cordovajs_path:function(libDir) {
        var jsPath = path.join(libDir, 'cordova-lib', 'cordova.js');
        return path.resolve(jsPath);
    },

    // Replace the www dir with contents of platform_www and app www.
    update_www:function() {
        var projectRoot = util.isCordova(this.path);
        var app_www = util.projectWww(projectRoot);
        var platform_www = path.join(this.path, 'platform_www');

        // Clear the www dir
        shell.rm('-rf', this.www_dir());
        shell.mkdir(this.www_dir());
        // Copy over all app www assets
        shell.cp('-rf', path.join(app_www, '*'), this.www_dir());
        // Copy over stock platform www assets (cordova.js)
        shell.cp('-rf', path.join(platform_www, '*'), this.www_dir());
    },

    update_overrides: function() {
        var projectRoot = util.isCordova(this.path);
        var mergesPath = path.join(util.appDir(projectRoot), 'merges', 'firefoxos');
        if(fs.existsSync(mergesPath)) {
            var overrides = path.join(mergesPath, '*');
            shell.cp('-rf', overrides, this.www_dir());
        }
    },

    config_xml:function(){
        return path.join(this.path, 'config.xml');
    },

    // Returns a promise.
    update_project: function(cfg) {
        return this.update_from_config()
            .then(function(){
                this.update_overrides();
                util.deleteSvnFolders(this.www_dir());
            }.bind(this));
    }
};
<|endoftext|>


<commit_before>let fs = require('fs');
let fns=[];
fs.readFile('./src/ec-do-3.0.0-beta.1.js', (err, data) => {
    if (err) {
        //如 果 出 错， 输 出 错 误 日 志， 并 给 客 户 端 返 回“ Server Error”
        console.error(err);
    }
    else {
        let md="";
        let obj=new Function(data.toString().replace('let','let _ecObj=')+';return _ecObj')();
        Object.keys(obj).forEach((item,index)=>{
            //md+=`## ${index+1}.${item}\r\n`
            fns.push({
                index:index,
                name:item
            })
        })
        //fs.writeFileSync("./README.md", md);
    }
})

fs.readFile('./README.md', (err, data) => {
    if (err) {
        //如 果 出 错， 输 出 错 误 日 志， 并 给 客 户 端 返 回“ Server Error”
        console.error(err);
    }
    else {
        let _data=data.toString().split('\r\n');
        let i=1;
        _data.forEach((item,index)=>{
            if(/%%/.test(item)){
                _data[index]=item.replace('%%',i+'.');
                i++;
            }
        })
        fs.writeFileSync("./README.md", _data.join('\r\n'));
    }
})
<commit_msg>增加 fnIndex.js 可以自动生成序号<commit_after>let fs = require('fs');
let fns=[];
fs.readFile('./src/ec-do-3.0.0-beta.1.js', (err, data) => {
    if (err) {
        //如 果 出 错， 输 出 错 误 日 志， 并 给 客 户 端 返 回“ Server Error”
        console.error(err);
    }
    else {
        let md="";
        let obj=new Function(data.toString().replace('let','let _ecObj=')+';return _ecObj')();
        Object.keys(obj).forEach((item,index)=>{
            fns.push({
                index:index,
                name:item
            })
        })
    }
})

fs.readFile('./README.md', (err, data) => {
    if (err) {
        console.error(err);
    }
    else {
        let _data=data.toString().split('\r\n');
        let i=1;
        _data.forEach((item,index)=>{
            if(/%%/.test(item)){
                _data[index]=item.replace('%%',i+'.');
                i++;
            }
        })
        fs.writeFileSync("./README.md", _data.join('\r\n'));
    }
})
<|endoftext|>