Commits

Robert Brewer committed 027ceee

Moved html+js into a separate ui directory

  • Participants
  • Parent commits 642334c

Comments (0)

Files changed (13)

flowrate/__init__.py

 import os
 os.umask(000)
 thisdir = os.path.abspath(os.path.dirname(__file__))
+uidir = os.path.join(thisdir, 'ui')
 import sys
 
 import cherrypy
 
 
 txmanager = cherrypy.tools.staticfile.handler(
-    filename=os.path.join(thisdir, "transactions.html"))
+    filename=os.path.join(uidir, "transactions.html"))
 txmanager.GET = txmanager
 
 
     exposed = True
 
     def GET(self):
-        page = open(os.path.join(thisdir, "import.html"), "rb").read()
+        page = open(os.path.join(uidir, "import.html"), "rb").read()
         return page % {'body': '<p>Please select a Mint CSV file to import</p>'}
 
     def POST(self, csvfile):
         mi = csvutil.ErrorGatheringMintImporter()
         mi.load_mint_csv(csvfile.file)
 
-        page = open(os.path.join(thisdir, "import.html"), "rb").read()
+        page = open(os.path.join(uidir, "import.html"), "rb").read()
         errors = ["<tr><td>Unknown account name: %s</td></tr>" % a
                   for a in mi.unknown_accounts]
         if errors:
         cherrypy.response.status = 204
 
 flowsmanager = cherrypy.tools.staticfile.handler(
-    filename=os.path.join(thisdir, "flows.html"))
+    filename=os.path.join(uidir, "flows.html"))
 flowsmanager.GET = flowsmanager
 
 
 
 
 acctmanager = cherrypy.tools.staticfile.handler(
-    filename=os.path.join(thisdir, "accounts.html"))
+    filename=os.path.join(uidir, "accounts.html"))
 acctmanager.GET = acctmanager
 
 class Accounts(object):
 
 
 json2_js = cherrypy.tools.staticfile.handler(
-    filename=os.path.join(thisdir, "json2.js"))
+    filename=os.path.join(uidir, "json2.js"))
 json2_js.GET = json2_js
 
 common_js = cherrypy.tools.staticfile.handler(
-    filename=os.path.join(thisdir, "common.js"))
+    filename=os.path.join(uidir, "common.js"))
 common_js.GET = common_js
 
 class Root(object):

flowrate/accounts.html

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-    <title>Flowrate Accounts</title>
-
-<style type='text/css'>
-
-body {
-    margin: 0;
-    padding: 0;
-    font: 10pt Verdana, sans-serif;
-}
-
-.header {
-    padding: 0.25em 1em;
-    background-color: #8495C0;
-    color: white;
-    border-bottom: 1px solid #425367;
-}
-
-h1 {
-    font: 700 14pt Verdana, sans-serif;
-}
-
-h1 a {
-    color: #DDDDEE;
-    text-decoration: none;
-}
-
-#status {
-    position: fixed;
-    top: 0;
-    padding: 0.25em;
-    right: 0;
-    width: 50%;
-    height: 5em;
-    overflow: auto;
-}
-
-#status p {
-    margin: 0;
-    padding: 0;
-    width: 100%;
-    text-align: right;
-}
-
-#fatal_error {
-    visibility: hidden;
-    position: fixed;
-    top: 20px;
-    left: 20px;
-    right: 20px;
-    bottom: 20px;
-    background-color: #FFFFFF;
-    border: 2px solid DarkRed;
-    padding: 2em;
-    overflow: auto;
-    z-index: -1;
-}
-
-#fatal_error_title {
-    font: 700 18pt Verdana, sans-serif;
-    border-bottom: 1px solid DarkRed;
-    color: DarkRed;
-}
-
-#accounts {
-    padding: 0 0.25em;
-    border-collapse: collapse;
-}
-
-#accounts tr {
-    padding: 0;
-}
-
-#accounts th {
-    background-color: DarkGrey;
-    color: white;
-    padding: 0.25em;
-    margin: 0;
-}
-
-#accounts tr.acctrow {
-    padding: 0;
-    cursor: pointer;
-}
-
-#accounts td {
-    vertical-align: bottom;
-    padding: 0 0.25em;
-    border: 1px solid LightGrey;
-}
-
-#accounts tr#acctedit input[type=text] {
-    width: 100%;
-    background-color: #FFFFF0;
-    color: black;
-}
-
-</style>
-
-<script type="text/javascript" src="https://www.google.com/jsapi"></script>
-<script type="text/javascript" src="/json2.js"></script>
-<script type="text/javascript" src="/common.js"></script>
-<script type="text/javascript">
-
-google.load('visualization', '1.0', {'packages':['corechart']});
-
-//                                  ACCOUNTS                                  //
-
-
-function populate_accounts() {
-    var h = http("GET", "/accounts", false);
-    h[200] = function(h) {
-        var j = JSON.parse(h.responseText);
-        for (var i = 0; i < j.data.length; i++) {
-            add_acct_row(j.data[i]);
-        }
-    }
-    h.send();
-}
-
-accounts = {};
-
-function add_acct_row(acct) {
-    var r = null;
-    var d = null;
-    var a = null;
-    var i = null;
-
-    var acctid = segment(acct.id, -1);
-
-    accounts[acctid] = acct;
-
-    // Create acct row
-    r = document.createElement("tr");
-    r.className = 'acctrow';
-    r.id = 'acctrow' + acctid;
-    r.acctid = parseInt(segment(acctid, -1));
-    r.onclick = function () { edit_account(acctid) };
-
-    // Save button cell
-    d = document.createElement("td");
-    d.className = 'acctbuttons';
-    r.appendChild(d);
-
-    // ID cell
-    d = document.createElement("td");
-    d.className = 'acctid';
-    set_text(d, acctid);
-    r.appendChild(d);
-
-    // description
-    d = document.createElement("td");
-    d.className = 'acctname';
-    set_text(d, acct.name);
-    r.appendChild(d);
-
-    // description
-    d = document.createElement("td");
-    d.className = 'accttype';
-    set_text(d, acct.type);
-    r.appendChild(d);
-
-    // Insert the row in order by acct id ascending
-    var rowset = $("acctheader").parentNode;
-    for (var i=0; i < rowset.childNodes.length; i++) {
-        var existing = rowset.childNodes[i];
-        if (existing.className == 'acctrow') {
-            if (existing.acctid > r.acctid) {
-                rowset.insertBefore(r, existing);
-                break;
-            }
-        }
-    }
-    // If no match:
-    if (r.parentNode == null) rowset.appendChild(r);
-
-    return r;
-}
-
-var acctid_being_edited = null;
-
-function edit_account(acctid) {
-    // Unhide any previously-edited row
-    if (!(acctid_being_edited == null)) {
-        $('acctrow' + acctid_being_edited).style.display = 'table-row';
-    }
-
-    var acctedit = $('acctedit');
-
-    if (acctid == null) {
-        // Keep existing values to make it easy to copy an existing row
-        $('edit_id').disabled = false;
-        // Remove the row from its current position
-        acctedit.parentNode.removeChild(acctedit);
-        // Insert before the header row
-        $('acctheader').parentNode.insertBefore(acctedit, $('acctheader'));
-    } else {
-        var acct = accounts[acctid];
-        $('edit_id').value = acctid;
-        $('edit_id').disabled = true;
-        $('edit_name').value = acct.name;
-        $('edit_type').value = acct.type;
-
-        // Remove the row from its current position
-        acctedit.parentNode.removeChild(acctedit);
-        // Insert the edit row before the existing row, then hide the existing
-        var acctrow = $('acctrow' + acctid);
-        acctrow.parentNode.insertBefore(acctedit, acctrow);
-        acctrow.style.display = 'none';
-    }
-
-    acctid_being_edited = acctid;
-}
-
-function save_account() {
-    var acct = {};
-    acct.name = $('edit_name').value;
-    acct.type = $('edit_type').value;
-
-    if (acctid_being_edited == null) {
-        // New transaction
-        acct.id = '/accounts/' + $('edit_id').value;
-        var h = http("PUT", acct.id, false,
-                     "Saving new transaction");
-        h[201] = function(h) { add_acct_row(acct); };
-    } else {
-        // Update existing transaction
-        var h = http("PUT", '/accounts/' + acctid_being_edited, false,
-                     "Updating transaction " + acctid_being_edited);
-        // TODO: update cell data
-        h[200] = function(h) {};
-        h[204] = function(h) {};
-    }
-    h.setRequestHeader("Content-Type", "application/json");
-    h.send(JSON.stringify({"body": acct}));
-}
-
-//                                   OTHER                                   //
-
-balances = [];
-
-google.load('visualization', '1.0', {'packages':['corechart']});
-
-function drawChart() {
-    // Get the balances data
-    var h = http("GET", "/balances" + document.location.search, true,
-                 "Retrieving balance data...");
-    h[200] = function(h) {
-        var j = JSON.parse(h.responseText);
-        if (keys(j.data).length == 0) return;
-
-        for (var a in j.data) {
-            var dgdata = j.data[a];
-            var acctid = parseInt(a);
-            for (var i = 0; i < balances.length; i++) {
-                if (balances[i][0].indexOf(acctid) != -1) {
-                    var dgsum = balances[i][1];
-                    for (var dg in dgdata) {
-                        dgsum[dg] = (dgsum[dg] || 0) + dgdata[dg];
-                    }
-                }
-            }
-        }
-
-        var bc = drawAccountChart('Balances', balances, $('chart_div'));
-        var table = bc[0];
-        var chart = bc[1];
-
-        // Show transactions matching a clicked point or column (account set)
-        function handle_select() {
-            var selection = chart.getSelection();
-            var seldg = null;
-            var selaccts = null;
-            for (var i = 0; i < selection.length; i++) {
-                // We are assuming one selection for now
-                var item = selection[i];
-                if (item.row != null && item.column != null) {
-                    seldg = table.getValue(item.row, 0);
-                    selaccts = balances[item.column - 1][0];
-                } else if (item.row != null) {
-                    seldg = table.getValue(item.row, 0);
-                } else if (item.column != null) {
-                    selaccts = balances[item.column - 1][0];
-                }
-            }
-
-            var qs = [];
-            if (selaccts != null) qs.push("accounts=" + selaccts.join(","));
-            if (seldg == null) {
-                qs.push("years=" + query['years']);
-                qs.push("months=" + query['months']);
-                qs.push("days=" + query['days']);
-            } else {
-                qs.push("years=" + seldg.substring(0, 4));
-                if (seldg.length >= 7) {
-                    qs.push("months=" + parseInt(seldg.substring(5, 7)));
-                }
-                if (seldg.length >= 10) {
-                    qs.push("days=" + parseInt(seldg.substring(8, 10)));
-                }
-            }
-            window.open("/transactions/manager?" + qs.join("&"), "_blank");
-        }
-
-        google.visualization.events.addListener(chart, 'select', handle_select);
-
-    }
-    h.send();
-}
-
-query = {
-    'accounts': '',
-    'years': '',
-    'months': '',
-    'days': '',
-    };
-
-function init() {
-    var pairs = document.location.search.substring('?'.length).split('&');
-    var keyval = null;
-    for (var i = 0; i < pairs.length; i++) {
-        keyval = pairs[i].split('=');
-        query[unescape(keyval[0])] = unescape(keyval[1]);
-    }
-
-    $('accounts').value = query['accounts'];
-    $('years').value = query['years'];
-    $('months').value = query['months'];
-    $('days').value = query['days'];
-    $('dategroup').value = query['dategroup'];
-
-    var qs = 'accounts=' + encodeURIComponent(query['accounts']) +
-             '&years=' + encodeURIComponent(query['years']) +
-             '&months=' + encodeURIComponent(query['months']) +
-             '&days=' + encodeURIComponent(query['days']) +
-             '&dategroup=' + query['dategroup'];
-    $("txslink").href = $("txslink").href + "?" + qs;
-    $("flowslink").href = $("flowslink").href + "?" + qs;
-
-    populate_accounts();
-
-    var req_accounts = (query['accounts'] + ',' + query['credits'] + ','
-                        + query['debits']).split(",");
-    var acctsets = account_sets(req_accounts);
-    for (var i=0; i < acctsets.length; i++) {
-        balances.push([acctsets[i], {}])
-    }
-    drawChart();
-
-    edit_account(null);
-}
-
-</script>
-
-</head>
-
-<body onLoad="init()">
-<div class='header'>
-    <h1 id="banner">Flowrate:
-        Accounts
-        | <a id="txslink" href='/transactions/manager'>Transactions</a>
-        | <a id="flowslink" href='/flows/manager'>Flows</a>
-        | <a id="importlink" href="/import">Import</a>
-    </h1>
-    <div id="status"></div>
-    <p><form action='' method='GET'>
-        Accounts: <input type='text' id="accounts" name="accounts" value="" size="9" />
-        Years: <input type='text' id="years" name="years" value="" size="9" />
-        Months: <input type='text' id="months" name="months" value="" size="6" />
-        Days: <input type='text' id="days" name="days" value="" size="6" />
-        Group: <select id="dategroup" name="dategroup">
-               <option>year</option>
-               <option selected="selected">month</option>
-               <option>day</option>
-               </select>
-        <input type='submit' value="GO" />
-    </form></p>
-</div>
-
-<div id='fatal_error'>
-    <h2 id='fatal_error_title'></h2>
-    <p style='float: right'><input type="button" value="Close" onClick="remove_fatal()" /></p>
-    <p id='fatal_error_msg'></p>
-</div>
-
-<div id="chart_div"></div>
-
-<table id='accounts'>
-<tr id='acctedit'>
-    <td><input id='edit_save' type='button' value='Save' onClick='save_account()' /></td>
-    <td><input id='edit_id' type='text' value='ID' /></td>
-    <td><input id='edit_name' type='text' value="New account" /></td>
-    <td><select id='edit_type'>
-        <option>asset</option>
-        <option>liability</option>
-        <option>income</option>
-        <option>expense</option>
-        </select></td>
-</tr>
-<tr id='acctheader'>
-    <th><span onClick="edit_account(null)" title="New account" style="cursor: pointer">(+)</span></th>
-    <th>ID</th>
-    <th>Name</th>
-    <th>Type</th>
-</tr>
-</table>
-
-</body>
-</html>

flowrate/common.js

-//                                 UTILITIES                                 //
-
-function $(name) { return document.getElementById(name) };
-
-function commafy(text) {
-    return text.toString().replace(/\d(?=(?:\d\d\d)+(?!\d))/g, "$&,");
-}
-
-function truncate(text, len) {
-    if (text.length > len) text = text.substring(0, len - 3) + '...';
-    return text;
-}
-
-function option_text(options, value) {
-    // Return .text from the option whose .value matches the given value,
-    // otherwise return the original value
-    for (var i = 0; i < options.length; i++) {
-        if (options[i].value == value) {
-            return options[i].text;
-        }
-    }
-    return value;
-}
-
-function ISODate(d) {
-    var dy = d.getFullYear()
-    // Y2K
-    if (dy < 1970) dy = dy + 100;
-    var dm = (d.getMonth() + 1).toString();
-    if (dm.length < 2) dm = "0" + dm;
-    var dd = d.getDate().toString();
-    if (dd.length < 2) dd = "0" + dd;
-    return dy + "-" + dm + "-" + dd;
-}
-
-function keys(obj) {
-   var k = [];
-   for (var key in obj){
-      k.push(key);
-   }
-   return k;
-}
-
-function segment(str, index, separator) {
-    // Return the indexed segment of the given string.
-    if (typeof(separator) == 'undefined') separator = '/';
-    var a = str.split(separator);
-    if (index < 0) index = a.length + index;
-    return a[index];
-}
-
-function get_text(elem) {
-    if (elem.innerText != undefined) {
-        // Internet Explorer
-        return elem.innerText;
-    } else {
-        // Mozilla
-        return elem.textContent;
-    }
-}
-
-function set_text(elem, newvalue) {
-    if (elem.innerText != undefined) {
-        // Internet Explorer
-        elem.innerText = newvalue;
-    } else {
-        // Mozilla
-        elem.textContent = newvalue;
-    }
-}
-
-function http(method, url, async, msg) {
-    var h;
-    
-    if (typeof(XMLHttpRequest) != "undefined") {
-        h = new XMLHttpRequest();
-    } else {
-        try { h = new ActiveXObject("Msxml2.XMLHTTP"); }
-        catch (e) {
-            try { h = new ActiveXObject("Microsoft.XMLHTTP"); }
-            catch (E) { set_status("Your browser is not supported.", 5000, "DarkRed"); }
-        }
-    }
-    
-    if (msg != undefined) var m = set_status(msg);
-    
-    h.onreadystatechange = function() {
-        if (h.readyState == 4) {
-            if (msg != undefined) remove_status(m);
-            try {
-                var status = h.status;
-            } catch(e) {
-                var status = "NO HTTP RESPONSE";
-            }
-            if (status in h) {
-                // Use a custom handler (defined on the XMLHttpRequest object
-                //  itself by the caller of this function).
-                h[status](h)
-            } else {
-                // Use a default handler.
-                if (status >= 500) {
-                    var v = h.status.toString() + ' ' + h.statusText;
-                    var ct = h.getResponseHeader("Content-Type");
-                    var is_html = (ct && ct.indexOf("html") != -1);
-                    set_fatal("Failure (" + v + "): " + (msg ? msg : url),
-                              h.responseText, is_html);
-                } else {
-                    var v = h.status.toString() + ' ' + h.statusText;
-                    set_status("Failure (" + v + "): " + (msg ? msg : url),
-                               5000, "DarkRed");
-                }
-            }
-        }
-    }
-    
-    h.open(method, url, async);
-    return h
-}
-
-// ----------------------------- DOMAIN HELPERS ----------------------------- //
-
-function account_sets(req_accounts) {
-    // Return a list of lists of account ids from the given comma-separated
-    // string of account ranges. "1,3-5,1+5" returns [[1], [3, 4, 5], [1, 5]]
-    var acctsets = [];
-    for (var i=0; i < req_accounts.length; i++) {
-        // Strip out any chars besides 0-9, plus and dash
-        var linespec = req_accounts[i].replace(/[^0-9+-]/g, '')
-        if (linespec) {
-            var acctids = [];
-            var idranges = linespec.split('+');
-            for (var j=0; j < idranges.length; j++) {
-                var idrange = idranges[j].split("-", 2);
-                if (idrange.length == 2) {
-                    // "lo-hi" = a range of ids from lo to hi, inclusive
-                    var lo = parseInt(idrange[0]);
-                    var hi = parseInt(idrange[1]);
-                    if (isNaN(lo) || isNaN(hi) || lo >= hi) continue;
-                    for (var k=lo; k <= hi; k++) {
-                        if (acctids.indexOf(k) == -1) acctids.push(k);
-                    }
-                } else {
-                    // single id
-                    var k = parseInt(idrange[0]);
-                    if (isNaN(k)) continue;
-                    if (acctids.indexOf(k) == -1) acctids.push(k);
-                }
-            }
-            acctsets.push(acctids);
-        }
-    }
-    return acctsets;
-}
-
-function isSubAccount(child, parent) {
-    for (var i = 3; i >= 0; i--) {
-        var scale = Math.pow(10, i);
-        if ((parent % scale == 0) && (parent <= child) && (child < parent + scale)) {
-            return true;
-        }
-    }
-    return false;
-}
-
-
-//                                   STATUS                                   //
-
-function set_fatal(title, msg, is_html) {
-    set_text($("fatal_error_title"), title);
-    if (is_html) {
-        $("fatal_error_msg").innerHTML = msg;
-    } else {
-        set_text($("fatal_error_msg"), msg);
-    }
-    $("fatal_error").style.zIndex = 100;
-    $("fatal_error").style.visibility = 'visible';
-}
-
-function remove_fatal() {
-    set_text($("fatal_error_title"), "");
-    $("fatal_error_msg").innerHTML = "";
-    $("fatal_error").style.visibility = 'hidden';
-    $("fatal_error").style.zIndex = -1;
-}
-
-function set_status(msg, decay, color) {
-    var log = $("status");
-    var m = document.createElement("p");
-    if (color != undefined) m.style.backgroundColor = color;
-    set_text(m, msg);
-    log.appendChild(m);
-    if (!(decay != undefined)) decay = 5000;
-    setTimeout(function () { remove_status(m); }, decay);
-    return m;
-}
-
-function remove_status(msgelem) {
-    if (msgelem.parentNode) msgelem.parentNode.removeChild(msgelem);
-}
-
-//                                   CHART                                   //
-
-function drawAccountChart(title, data, element) {
-    // Draw a Google Chart onto the $chart_div element.
-    // The 'data' argument MUST be an array of (account set, dategroup) pairs.
-    // Each account set must an array of account id's (integers), which will
-    // be used as columns in the DataTable, which then are drawn as individual
-    // lines/bars in the resulting chart.
-    // Each dategroup is an object with string keys that describe a date group,
-    // (such as '2012-01' for the month of January, 2012), which will be used
-    // as rows in the DataTable, which then are drawn as individual points
-    // on the horizontal axis.
-
-    // Create the data table.
-    var table = new google.visualization.DataTable();
-    table.addColumn('string', 'Account');
-
-    // Add columns from our data
-    var series = {};
-    var dategroups = [];
-    for (var i = 0; i < data.length; i++) {
-        var col = data[i];
-        var accountset = col[0]
-        if (accountset.length > 1) {
-            // Draw a line, label it with ids only
-            table.addColumn('number', accountset.join(" + "));
-            series[i] = {"type": "line", "curveType": "function"};
-        } else {
-            // Draw bars, label it with id and name
-            var a = accountset[0];
-            var name = (accounts[a] ? accounts[a].name : '');
-            table.addColumn('number', a + ': ' + name);
-        }
-
-        // Grab the row keys and give them a stable sort order
-        if (i == 0) {
-            for (var dg in col[1]) {
-                dategroups.push(dg);
-            }
-            dategroups.sort();
-        }
-    }
-
-    // Add rows from our data
-    for (var i = 0; i < dategroups.length; i++) {
-        var dg = dategroups[i];
-        var row = [dg];
-        for (var a = 0; a < data.length; a++) {
-            row.push(data[a][1][dg] || 0);
-        }
-        table.addRow(row);
-    }
-
-    // For some reason, we have to do this after we add all the rows
-    var formatter = new google.visualization.NumberFormat(
-        {prefix: '$', negativeColor: 'red', negativeParens: true});
-    for (var i = 0; i < data.length; i++) {
-        formatter.format(table, i + 1);
-    }
-
-    var width = dategroups.length * 75;
-    if (width < 600) { width = 600; }
-    if (width > 1200) { width = 1200; }
-    var options = {'title': title,
-                   'width': width, 'height': 400,
-                   'vAxis': {'minValue': 0},
-                   'seriesType': "bars",
-                   'series': series
-                   };
-
-    // Instantiate and draw our chart, passing in some options.
-    var chart = new google.visualization.ComboChart(element);
-    chart.draw(table, options);
-
-    return [table, chart];
-}
-

flowrate/flows.html

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-    <title>Flowrate Flows</title>
-
-<style type='text/css'>
-
-body {
-    margin: 0;
-    padding: 0;
-    font: 10pt Verdana, sans-serif;
-}
-
-.header {
-    padding: 0.25em 1em;
-    background-color: #8495C0;
-    color: white;
-    border-bottom: 1px solid #425367;
-}
-
-h1 {
-    font: 700 14pt Verdana, sans-serif;
-}
-
-h1 a {
-    color: #DDDDEE;
-    text-decoration: none;
-}
-
-#status {
-    position: fixed;
-    top: 0;
-    padding: 0.25em;
-    right: 0;
-    width: 50%;
-    height: 3em;
-    overflow: auto;
-}
-
-#status p {
-    margin: 0;
-    padding: 0;
-    width: 100%;
-    text-align: right;
-}
-
-#fatal_error {
-    visibility: hidden;
-    position: fixed;
-    top: 20px;
-    left: 20px;
-    right: 20px;
-    bottom: 20px;
-    background-color: #FFFFFF;
-    border: 2px solid DarkRed;
-    padding: 2em;
-    overflow: auto;
-    z-index: -1;
-}
-
-#fatal_error_title {
-    font: 700 18pt Verdana, sans-serif;
-    border-bottom: 1px solid DarkRed;
-    color: DarkRed;
-}
-
-#flows {
-    padding: 0 0.25em;
-    border-collapse: collapse;
-    width: 100%;
-}
-
-#flows tr {
-    padding: 0;
-}
-
-#flows th {
-    background-color: DarkGrey;
-    color: white;
-    padding: 0.25em;
-    margin: 0;
-}
-
-#flows tr.flowrow {
-    padding: 0;
-    cursor: pointer;
-}
-
-#flows td {
-    vertical-align: bottom;
-    padding: 0 0.25em;
-    border: 1px solid LightGrey;
-}
-
-#flows td a {
-    cursor: default;
-}
-
-#flows td.flowamount {
-    text-align: right;
-    font-family: monospace;
-}
-
-#flows tr#flowedit input[type=text] {
-    width: 100%;
-    background-color: #FFFFF0;
-    color: black;
-}
-
-</style>
-
-<script type="text/javascript" src="/json2.js"></script>
-<script type="text/javascript" src="/common.js"></script>
-<script type="text/javascript">
-
-//                                   FLOWS                                   //
-
-accounts = {};
-
-function populate_accounts() {
-    var h = http("GET", "/accounts", false);
-    h[200] = function(h) {
-        var j = JSON.parse(h.responseText);
-        for (var i = 0; i < j.data.length; i++) {
-            var a = j.data[i];
-
-            var acctid = segment(a.id, -1);
-            accounts[acctid] = a;
-
-            var c = document.createElement("option");
-            c.text = truncate(a.name, 20);
-            c.value = a.id;
-            $('edit_credit').options.add(c);
-
-            var d = document.createElement("option");
-            d.text = truncate(a.name, 20);
-            d.value = a.id;
-            $('edit_debit').options.add(d);
-        }
-    }
-    h.send();
-}
-
-function populate_flows() {
-    var h = http("GET", "/flows" + document.location.search, true);
-    h[200] = function(h) {
-        var j = JSON.parse(h.responseText);
-        if (j.data) {
-            for (var i = 0; i < j.data.length; i++) {
-                add_flow_row(j.data[i]);
-            }
-        }
-    }
-    h.send();
-}
-
-flows = {};
-
-function add_flow_row(flow) {
-    var r = null;
-    var d = null;
-    var a = null;
-    var i = null;
-
-    var flowid = segment(flow.id, -1);
-
-    flows[flowid] = flow;
-
-    // Create flow row
-    r = document.createElement("tr");
-    r.className = 'flowrow';
-    r.id = 'flowrow' + flowid;
-    r.flow = flow;
-    r.onclick = function () { edit_flow(flowid) };
-    
-    // ID cell
-    d = document.createElement("td");
-    d.className = 'flowid';
-    set_text(d, flowid);
-    r.appendChild(d);
-    
-    // start
-    d = document.createElement("td");
-    d.className = 'flowstart';
-    set_text(d, flow.start);
-    r.appendChild(d);
-    
-    // end
-    d = document.createElement("td");
-    d.className = 'flowend';
-    set_text(d, flow.end);
-    r.appendChild(d);
-    
-    // period
-    d = document.createElement("td");
-    d.className = 'flowperiod';
-    set_text(d, flow.period);
-    r.appendChild(d);
-    
-    // unit
-    d = document.createElement("td");
-    d.className = 'flowunit';
-    set_text(d, flow.unit);
-    r.appendChild(d);
-    
-    // days
-    d = document.createElement("td");
-    d.className = 'flowday';
-    set_text(d, flow.days.join(","));
-    r.appendChild(d);
-    
-    // credit
-    d = document.createElement("td");
-    d.className = 'flowcredit';
-    r.appendChild(d);
-    a = document.createElement("a");
-    a.href = flow.credit;
-    set_text(a, option_text($('edit_credit').options, flow.credit));
-    d.appendChild(a);
-    
-    // debit
-    d = document.createElement("td");
-    d.className = 'flowdebit';
-    r.appendChild(d);
-    a = document.createElement("a");
-    a.href = flow.debit;
-    set_text(a, option_text($('edit_debit').options, flow.debit));
-    d.appendChild(a);
-    
-    // description
-    d = document.createElement("td");
-    d.className = 'flowdesc';
-    set_text(d, flow.description);
-    r.appendChild(d);
-    
-    // amount
-    d = document.createElement("td");
-    d.className = 'flowamount';
-    set_text(d, '$' + commafy(flow.amount.toFixed(2)));
-    r.appendChild(d);
-    
-    // Insert the row in order by postdate descending
-    var rowset = $("flowheader").parentNode;
-    for (var i=0; i < rowset.childNodes.length; i++) {
-        var existing = rowset.childNodes[i];
-        if (existing.className == 'flowrow') {
-            if (existing.flow.debit <= flow.debit) {
-                rowset.insertBefore(r, existing);
-                break;
-            }
-        }
-    }
-    // If no match:
-    if (r.parentNode == null) rowset.appendChild(r);
-
-    return r;
-}
-
-var flowid_being_edited = null;
-
-function edit_flow(flowid) {
-    // Unhide any previously-edited row
-    if (!(flowid_being_edited == null)) {
-        $('flowrow' + flowid_being_edited).style.display = 'table-row';
-    }
-
-    var flowedit = $('flowedit');
-
-    if (flowid == null) {
-        // Keep existing values to make it easy to copy an existing row
-        // Remove the row from its current position
-        flowedit.parentNode.removeChild(flowedit);
-        // Insert before the header row
-        $('flowheader').parentNode.insertBefore(flowedit, $('flowheader'));
-    } else {
-        var flow = flows[flowid];
-        $('edit_start').value = flow.start;
-        $('edit_end').value = flow.end;
-        $('edit_period').value = flow.period;
-        $('edit_unit').value = flow.unit;
-        $('edit_days').value = flow.days.join(",");
-        $('edit_credit').value = flow.credit;
-        $('edit_debit').value = flow.debit;
-        $('edit_description').value = flow.description;
-        $('edit_amount').value = flow.amount;
-
-        // Remove the row from its current position
-        flowedit.parentNode.removeChild(flowedit);
-        // Insert the edit row before the existing row, then hide the existing
-        var flowrow = $('flowrow' + flowid);
-        flowrow.parentNode.insertBefore(flowedit, flowrow);
-        flowrow.style.display = 'none';
-    }
-
-    flowid_being_edited = flowid;
-}
-
-function save_flow() {
-    var flow = {};
-    flow.start = $('edit_start').value;
-    flow.end = $('edit_end').value;
-    flow.period = $('edit_period').value;
-    flow.unit = $('edit_unit').value;
-    flow.days = [];
-    var d = $('edit_days').value.split(",");
-    for (var i = 0; i < d.length; i++) {
-        flow.days.push(parseInt(d[i]));
-    }
-    flow.credit = $('edit_credit').value;
-    flow.debit = $('edit_debit').value;
-    flow.description = $('edit_description').value;
-    flow.amount = parseFloat($('edit_amount').value.replace(/[^0-9.]/g, ''));
-
-    if (flowid_being_edited == null) {
-        // New flow
-        var h = http("POST", '/flows', false, "Saving new flow");
-        // TODO: add new table row
-        h[201] = function(h) {
-            flow.id = h.getResponseHeader("Location");
-            add_flow_row(flow);
-        };
-    } else {
-        // Update existing flow
-        var h = http("PUT", '/flows/' + flowid_being_edited, false,
-                     "Updating flow " + flowid_being_edited);
-        h[204] = function(h) {
-            // Remove and re-add the row. This will also move it if dates change
-            var flowrow = $('flowrow' + flowid_being_edited);
-            flowrow.parentNode.removeChild(flowrow);
-            flow.id = flowid_being_edited;
-            flowrow = add_flow_row(flow);
-
-            // Also move the flowedit row before it
-            var flowedit = $('flowedit');
-            flowedit.parentNode.removeChild(flowedit);
-            flowrow.parentNode.insertBefore(flowedit, flowrow);
-            flowrow.style.display = 'none';
-        };
-    }
-    h.setRequestHeader("Content-Type", "application/json");
-    h.send(JSON.stringify({"body": flow}));
-}
-
-function calc_edit_amount(e) {
-    try { if (e.value[0] == "=") e.value = eval(e.value.substring(1)); } catch(e) {}
-}
-
-//                                   OTHER                                   //
-
-query = {
-    'accounts': '',
-    'credits': '',
-    'debits': '',
-    'years': '',
-    'months': '',
-    'days': '',
-    'dategroup': 'month',
-    };
-
-function init() {
-    var pairs = document.location.search.substring('?'.length).split('&');
-    var keyval = null;
-    for (var i = 0; i < pairs.length; i++) {
-        keyval = pairs[i].split('=');
-        query[unescape(keyval[0])] = unescape(keyval[1]);
-    }
-
-    $('accounts').value = query['accounts'];
-    $('credits').value = query['credits'];
-    $('debits').value = query['debits'];
-    $('years').value = query['years'];
-    $('months').value = query['months'];
-    $('days').value = query['days'];
-
-    qs = 'accounts=' + encodeURIComponent(query['accounts']) +
-         '&years=' + encodeURIComponent(query['years']) +
-         '&months=' + encodeURIComponent(query['months']) +
-         '&dategroup=' + query['dategroup'];
-    $("accountslink").href = $("accountslink").href + "?" + qs;
-    $("txslink").href = $("txslink").href + "?" + qs;
-
-    populate_accounts();
-    populate_flows();
-
-    edit_flow(null);
-    $('edit_start').value = ISODate(new Date());
-}
-
-</script>
-</head>
-
-<body onLoad="init()">
-<div class='header'>
-    <h1 id="banner">Flowrate:
-        <a id="accountslink" href='/accounts/manager'>Accounts</a>
-        | <a id="txslink" href='/transactions/manager'>Transactions</a>
-        | Flows
-        | <a id="importlink" href="/import">Import</a>
-    </h1>
-    <div id="status"></div>
-    <p><form action='' method='GET'>
-        Accounts: <input type='text' id="accounts" name="accounts" value="" size="9" />
-        Credits: <input type='text' id="credits" name="credits" value="" size="9" />
-        Debits: <input type='text' id="debits" name="debits" value="" size="9" />
-        Years: <input type='text' id="years" name="years" value="" size="9" />
-        Month: <input type='text' id="months" name="months" value="" size="6" />
-        Days: <input type='text' id="days" name="days" value="" size="6" />
-        <input type='submit' value="GO" />
-    </form></p>
-</div>
-
-<div id='fatal_error'>
-    <h2 id='fatal_error_title'></h2>
-    <p style='float: right'><input type="button" value="Close" onClick="remove_fatal()" /></p>
-    <p id='fatal_error_msg'></p>
-</div>
-
-<table id='flows'>
-<tr id='flowedit'>
-    <td><input id='edit_save' type='button' value='Save' onClick='save_flow()' /></td>
-    <td><input id='edit_start' type='text' size='10' /></td>
-    <td><input id='edit_end' type='text' size='10' /></td>
-    <td><input id='edit_period' type='text' size='3' /></td>
-    <td><select id='edit_unit'>
-        <option>years</option>
-        <option selected="selected">months</option>
-        <option>weeks</option>
-        </select></td>
-    <td><input id='edit_days' type='text' size='4' /></td>
-    <td><select id='edit_credit'></select></td>
-    <td><select id='edit_debit'></select></td>
-    <td><input id='edit_description' type='text' value="New flow" /></td>
-    <td><input id='edit_amount' type='text' size='10' style="text-align: right" value="0.00" 
-               onBlur='calc_edit_amount(this)' /></td>
-</tr>
-<tr id='flowheader'>
-    <th>ID&nbsp;<span onClick="edit_flow(null)" title="New flow" style="cursor: pointer">(+)</span></th>
-    <th>Start&nbsp;Date</th>
-    <th>End&nbsp;Date</th>
-    <th>Period</th>
-    <th>Unit</th>
-    <th>Days</th>
-    <th>Credit</th>
-    <th>Debit</th>
-    <th>Description</th>
-    <th>Amount</th>
-</tr>
-</table>
-</body>
-</html>

flowrate/import.html

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-    <title>Flowrate Import</title>
-
-<style type='text/css'>
-
-body {
-    margin: 0;
-    padding: 0;
-    font: 10pt Verdana, sans-serif;
-}
-
-.header {
-    padding: 0.25em 1em;
-    background-color: #8495C0;
-    color: white;
-    border-bottom: 1px solid #425367;
-}
-
-h1 {
-    font: 700 14pt Verdana, sans-serif;
-}
-
-h1 a {
-    color: #DDDDEE;
-    text-decoration: none;
-}
-#transactions {
-    padding: 0 0.25em;
-    border-collapse: collapse;
-    width: 100%%;
-}
-
-#transactions tr {
-    padding: 0;
-}
-
-#transactions th {
-    background-color: DarkGrey;
-    color: white;
-    padding: 0.25em;
-    margin: 0;
-}
-
-#transactions td {
-    vertical-align: bottom;
-    padding: 0 0.25em;
-    border: 1px solid LightGrey;
-}
-
-</style>
-</head>
-
-<body>
-<div class='header'>
-    <h1 id="banner">Flowrate:
-        <a id="accountslink" href='/accounts/manager'>Accounts</a>
-        | <a id="flowslink" href='/transactions/manager'>Transactions
-        | <a id="flowslink" href='/flows/manager'>Flows</a>
-        | Import
-    </h1>
-    <div id="status"></div>
-    <div>
-        <form action='' method='POST' enctype='multipart/form-data'>
-            File: <input type='file' id="csvfile" name="csvfile" size="30" />
-            <input type='submit' value="GO" />
-        </form>
-    </div>
-</div>
-
-%(body)s
-</body>
-</html>

flowrate/json2.js

-/*
-    http://www.JSON.org/json2.js
-    2011-02-23
-
-    Public Domain.
-
-    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
-
-    See http://www.JSON.org/js.html
-
-
-    This code should be minified before deployment.
-    See http://javascript.crockford.com/jsmin.html
-
-    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
-    NOT CONTROL.
-
-
-    This file creates a global JSON object containing two methods: stringify
-    and parse.
-
-        JSON.stringify(value, replacer, space)
-            value       any JavaScript value, usually an object or array.
-
-            replacer    an optional parameter that determines how object
-                        values are stringified for objects. It can be a
-                        function or an array of strings.
-
-            space       an optional parameter that specifies the indentation
-                        of nested structures. If it is omitted, the text will
-                        be packed without extra whitespace. If it is a number,
-                        it will specify the number of spaces to indent at each
-                        level. If it is a string (such as '\t' or '&nbsp;'),
-                        it contains the characters used to indent at each level.
-
-            This method produces a JSON text from a JavaScript value.
-
-            When an object value is found, if the object contains a toJSON
-            method, its toJSON method will be called and the result will be
-            stringified. A toJSON method does not serialize: it returns the
-            value represented by the name/value pair that should be serialized,
-            or undefined if nothing should be serialized. The toJSON method
-            will be passed the key associated with the value, and this will be
-            bound to the value
-
-            For example, this would serialize Dates as ISO strings.
-
-                Date.prototype.toJSON = function (key) {
-                    function f(n) {
-                        // Format integers to have at least two digits.
-                        return n < 10 ? '0' + n : n;
-                    }
-
-                    return this.getUTCFullYear()   + '-' +
-                         f(this.getUTCMonth() + 1) + '-' +
-                         f(this.getUTCDate())      + 'T' +
-                         f(this.getUTCHours())     + ':' +
-                         f(this.getUTCMinutes())   + ':' +
-                         f(this.getUTCSeconds())   + 'Z';
-                };
-
-            You can provide an optional replacer method. It will be passed the
-            key and value of each member, with this bound to the containing
-            object. The value that is returned from your method will be
-            serialized. If your method returns undefined, then the member will
-            be excluded from the serialization.
-
-            If the replacer parameter is an array of strings, then it will be
-            used to select the members to be serialized. It filters the results
-            such that only members with keys listed in the replacer array are
-            stringified.
-
-            Values that do not have JSON representations, such as undefined or
-            functions, will not be serialized. Such values in objects will be
-            dropped; in arrays they will be replaced with null. You can use
-            a replacer function to replace those with JSON values.
-            JSON.stringify(undefined) returns undefined.
-
-            The optional space parameter produces a stringification of the
-            value that is filled with line breaks and indentation to make it
-            easier to read.
-
-            If the space parameter is a non-empty string, then that string will
-            be used for indentation. If the space parameter is a number, then
-            the indentation will be that many spaces.
-
-            Example:
-
-            text = JSON.stringify(['e', {pluribus: 'unum'}]);
-            // text is '["e",{"pluribus":"unum"}]'
-
-
-            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
-            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
-
-            text = JSON.stringify([new Date()], function (key, value) {
-                return this[key] instanceof Date ?
-                    'Date(' + this[key] + ')' : value;
-            });
-            // text is '["Date(---current time---)"]'
-
-
-        JSON.parse(text, reviver)
-            This method parses a JSON text to produce an object or array.
-            It can throw a SyntaxError exception.
-
-            The optional reviver parameter is a function that can filter and
-            transform the results. It receives each of the keys and values,
-            and its return value is used instead of the original value.
-            If it returns what it received, then the structure is not modified.
-            If it returns undefined then the member is deleted.
-
-            Example:
-
-            // Parse the text. Values that look like ISO date strings will
-            // be converted to Date objects.
-
-            myData = JSON.parse(text, function (key, value) {
-                var a;
-                if (typeof value === 'string') {
-                    a =
-/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
-                    if (a) {
-                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
-                            +a[5], +a[6]));
-                    }
-                }
-                return value;
-            });
-
-            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
-                var d;
-                if (typeof value === 'string' &&
-                        value.slice(0, 5) === 'Date(' &&
-                        value.slice(-1) === ')') {
-                    d = new Date(value.slice(5, -1));
-                    if (d) {
-                        return d;
-                    }
-                }
-                return value;
-            });
-
-
-    This is a reference implementation. You are free to copy, modify, or
-    redistribute.
-*/
-
-/*jslint evil: true, strict: false, regexp: false */
-
-/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
-    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
-    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
-    lastIndex, length, parse, prototype, push, replace, slice, stringify,
-    test, toJSON, toString, valueOf
-*/
-
-
-// Create a JSON object only if one does not already exist. We create the
-// methods in a closure to avoid creating global variables.
-
-var JSON;
-if (!JSON) {
-    JSON = {};
-}
-
-(function () {
-    "use strict";
-
-    function f(n) {
-        // Format integers to have at least two digits.
-        return n < 10 ? '0' + n : n;
-    }
-
-    if (typeof Date.prototype.toJSON !== 'function') {
-
-        Date.prototype.toJSON = function (key) {
-
-            return isFinite(this.valueOf()) ?
-                this.getUTCFullYear()     + '-' +
-                f(this.getUTCMonth() + 1) + '-' +
-                f(this.getUTCDate())      + 'T' +
-                f(this.getUTCHours())     + ':' +
-                f(this.getUTCMinutes())   + ':' +
-                f(this.getUTCSeconds())   + 'Z' : null;
-        };
-
-        String.prototype.toJSON      =
-            Number.prototype.toJSON  =
-            Boolean.prototype.toJSON = function (key) {
-                return this.valueOf();
-            };
-    }
-
-    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
-        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
-        gap,
-        indent,
-        meta = {    // table of character substitutions
-            '\b': '\\b',
-            '\t': '\\t',
-            '\n': '\\n',
-            '\f': '\\f',
-            '\r': '\\r',
-            '"' : '\\"',
-            '\\': '\\\\'
-        },
-        rep;
-
-
-    function quote(string) {
-
-// If the string contains no control characters, no quote characters, and no
-// backslash characters, then we can safely slap some quotes around it.
-// Otherwise we must also replace the offending characters with safe escape
-// sequences.
-
-        escapable.lastIndex = 0;
-        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
-            var c = meta[a];
-            return typeof c === 'string' ? c :
-                '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
-        }) + '"' : '"' + string + '"';
-    }
-
-
-    function str(key, holder) {
-
-// Produce a string from holder[key].
-
-        var i,          // The loop counter.
-            k,          // The member key.
-            v,          // The member value.
-            length,
-            mind = gap,
-            partial,
-            value = holder[key];
-
-// If the value has a toJSON method, call it to obtain a replacement value.
-
-        if (value && typeof value === 'object' &&
-                typeof value.toJSON === 'function') {
-            value = value.toJSON(key);
-        }
-
-// If we were called with a replacer function, then call the replacer to
-// obtain a replacement value.
-
-        if (typeof rep === 'function') {
-            value = rep.call(holder, key, value);
-        }
-
-// What happens next depends on the value's type.
-
-        switch (typeof value) {
-        case 'string':
-            return quote(value);
-
-        case 'number':
-
-// JSON numbers must be finite. Encode non-finite numbers as null.
-
-            return isFinite(value) ? String(value) : 'null';
-
-        case 'boolean':
-        case 'null':
-
-// If the value is a boolean or null, convert it to a string. Note:
-// typeof null does not produce 'null'. The case is included here in
-// the remote chance that this gets fixed someday.
-
-            return String(value);
-
-// If the type is 'object', we might be dealing with an object or an array or
-// null.
-
-        case 'object':
-
-// Due to a specification blunder in ECMAScript, typeof null is 'object',
-// so watch out for that case.
-
-            if (!value) {
-                return 'null';
-            }
-
-// Make an array to hold the partial results of stringifying this object value.
-
-            gap += indent;
-            partial = [];
-
-// Is the value an array?
-
-            if (Object.prototype.toString.apply(value) === '[object Array]') {
-
-// The value is an array. Stringify every element. Use null as a placeholder
-// for non-JSON values.
-
-                length = value.length;
-                for (i = 0; i < length; i += 1) {
-                    partial[i] = str(i, value) || 'null';
-                }
-
-// Join all of the elements together, separated with commas, and wrap them in
-// brackets.
-
-                v = partial.length === 0 ? '[]' : gap ?
-                    '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
-                    '[' + partial.join(',') + ']';
-                gap = mind;
-                return v;
-            }
-
-// If the replacer is an array, use it to select the members to be stringified.
-
-            if (rep && typeof rep === 'object') {
-                length = rep.length;
-                for (i = 0; i < length; i += 1) {
-                    if (typeof rep[i] === 'string') {
-                        k = rep[i];
-                        v = str(k, value);
-                        if (v) {
-                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
-                        }
-                    }
-                }
-            } else {
-
-// Otherwise, iterate through all of the keys in the object.
-
-                for (k in value) {
-                    if (Object.prototype.hasOwnProperty.call(value, k)) {
-                        v = str(k, value);
-                        if (v) {
-                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
-                        }
-                    }
-                }
-            }
-
-// Join all of the member texts together, separated with commas,
-// and wrap them in braces.
-
-            v = partial.length === 0 ? '{}' : gap ?
-                '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
-                '{' + partial.join(',') + '}';
-            gap = mind;
-            return v;
-        }
-    }
-
-// If the JSON object does not yet have a stringify method, give it one.
-
-    if (typeof JSON.stringify !== 'function') {
-        JSON.stringify = function (value, replacer, space) {
-
-// The stringify method takes a value and an optional replacer, and an optional
-// space parameter, and returns a JSON text. The replacer can be a function
-// that can replace values, or an array of strings that will select the keys.
-// A default replacer method can be provided. Use of the space parameter can
-// produce text that is more easily readable.
-
-            var i;
-            gap = '';
-            indent = '';
-
-// If the space parameter is a number, make an indent string containing that
-// many spaces.
-
-            if (typeof space === 'number') {
-                for (i = 0; i < space; i += 1) {
-                    indent += ' ';
-                }
-
-// If the space parameter is a string, it will be used as the indent string.
-
-            } else if (typeof space === 'string') {
-                indent = space;
-            }
-
-// If there is a replacer, it must be a function or an array.
-// Otherwise, throw an error.
-
-            rep = replacer;
-            if (replacer && typeof replacer !== 'function' &&
-                    (typeof replacer !== 'object' ||
-                    typeof replacer.length !== 'number')) {
-                throw new Error('JSON.stringify');
-            }
-
-// Make a fake root object containing our value under the key of ''.
-// Return the result of stringifying the value.
-
-            return str('', {'': value});
-        };
-    }
-
-
-// If the JSON object does not yet have a parse method, give it one.
-
-    if (typeof JSON.parse !== 'function') {
-        JSON.parse = function (text, reviver) {
-
-// The parse method takes a text and an optional reviver function, and returns
-// a JavaScript value if the text is a valid JSON text.
-
-            var j;
-
-            function walk(holder, key) {
-
-// The walk method is used to recursively walk the resulting structure so
-// that modifications can be made.
-
-                var k, v, value = holder[key];
-                if (value && typeof value === 'object') {
-                    for (k in value) {
-                        if (Object.prototype.hasOwnProperty.call(value, k)) {
-                            v = walk(value, k);
-                            if (v !== undefined) {
-                                value[k] = v;
-                            } else {
-                                delete value[k];
-                            }
-                        }
-                    }
-                }
-                return reviver.call(holder, key, value);
-            }
-
-
-// Parsing happens in four stages. In the first stage, we replace certain
-// Unicode characters with escape sequences. JavaScript handles many characters
-// incorrectly, either silently deleting them, or treating them as line endings.
-
-            text = String(text);
-            cx.lastIndex = 0;
-            if (cx.test(text)) {
-                text = text.replace(cx, function (a) {
-                    return '\\u' +
-                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
-                });
-            }
-
-// In the second stage, we run the text against regular expressions that look
-// for non-JSON patterns. We are especially concerned with '()' and 'new'
-// because they can cause invocation, and '=' because it can cause mutation.
-// But just to be safe, we want to reject all unexpected forms.
-
-// We split the second stage into 4 regexp operations in order to work around
-// crippling inefficiencies in IE's and Safari's regexp engines. First we
-// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
-// replace all simple value tokens with ']' characters. Third, we delete all
-// open brackets that follow a colon or comma or that begin the text. Finally,
-// we look to see that the remaining characters are only whitespace or ']' or
-// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
-
-            if (/^[\],:{}\s]*$/
-                    .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
-                        .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
-                        .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
-
-// In the third stage we use the eval function to compile the text into a
-// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
-// in JavaScript: it can begin a block or an object literal. We wrap the text
-// in parens to eliminate the ambiguity.
-
-                j = eval('(' + text + ')');
-
-// In the optional fourth stage, we recursively walk the new structure, passing
-// each name/value pair to a reviver function for possible transformation.
-
-                return typeof reviver === 'function' ?
-                    walk({'': j}, '') : j;
-            }
-
-// If the text is not JSON parseable, then a SyntaxError is thrown.
-
-            throw new SyntaxError('JSON.parse');
-        };
-    }
-}());
-

flowrate/transactions.html

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-    <title>Flowrate Transactions</title>
-
-<style type='text/css'>
-
-body {
-    margin: 0;
-    padding: 0;
-    font: 10pt Verdana, sans-serif;
-}
-
-.header {
-    padding: 0.25em 1em;
-    background-color: #8495C0;
-    color: white;
-    border-bottom: 1px solid #425367;
-}
-
-h1 {
-    font: 700 14pt Verdana, sans-serif;
-}
-
-h1 a {
-    color: #DDDDEE;
-    text-decoration: none;
-}
-
-#status {
-    position: fixed;
-    top: 0;
-    padding: 0.25em;
-    right: 0;
-    width: 50%;
-    height: 3em;
-    overflow: auto;
-}
-
-#status p {
-    margin: 0;
-    padding: 0;
-    width: 100%;
-    text-align: right;
-}
-
-#fatal_error {
-    visibility: hidden;
-    position: fixed;
-    top: 20px;
-    left: 20px;
-    right: 20px;
-    bottom: 20px;
-    background-color: #FFFFFF;
-    border: 2px solid DarkRed;
-    padding: 2em;
-    overflow: auto;
-    z-index: -1;
-}
-
-#fatal_error_title {
-    font: 700 18pt Verdana, sans-serif;
-    border-bottom: 1px solid DarkRed;
-    color: DarkRed;
-}
-
-#transactions {
-    padding: 0 0.25em;
-    border-collapse: collapse;
-    width: 100%;
-}
-
-#transactions tr {
-    padding: 0;
-}
-
-#transactions th {
-    background-color: DarkGrey;
-    color: white;
-    padding: 0.25em;
-    margin: 0;
-}
-
-#transactions tr.txrow {
-    padding: 0;
-    cursor: pointer;
-}
-
-#transactions tr.flowrow {
-    padding: 0;
-    background-color: #EEFFEE;
-}
-
-#transactions td {
-    vertical-align: bottom;
-    padding: 0 0.25em;
-    border: 1px solid LightGrey;
-}
-
-#transactions td a {
-    cursor: default;
-}
-
-#transactions td.txamount {
-    text-align: right;
-    font-family: monospace;
-}
-
-#transactions tr#txedit input[type=text] {
-    width: 100%;
-    background-color: #FFFFF0;
-    color: black;
-}
-
-</style>
-
-<script type="text/javascript" src="https://www.google.com/jsapi"></script>
-<script type="text/javascript" src="/json2.js"></script>
-<script type="text/javascript" src="/common.js"></script>
-<script type="text/javascript">
-
-google.load('visualization', '1.0', {'packages':['corechart']});
-
-
-//                                TRANSACTIONS                                //
-
-
-accounts = {};
-
-function populate_accounts() {
-    var h = http("GET", "/accounts", false);
-    h[200] = function(h) {
-        var j = JSON.parse(h.responseText);
-        for (var i = 0; i < j.data.length; i++) {
-            var a = j.data[i];
-
-            var acctid = segment(a.id, -1);
-            accounts[acctid] = a;
-
-            var c = document.createElement("option");
-            c.text = truncate(a.name, 20);
-            c.value = a.id;
-            $('edit_credit').options.add(c);
-
-            var d = document.createElement("option");
-            d.text = truncate(a.name, 20);
-            d.value = a.id;
-            $('edit_debit').options.add(d);
-        }
-    }
-    h.send();
-}
-
-function populate_transactions() {
-    var h = http("GET", "/transactions" + location.search, true,
-                 "Loading transactions...");
-    h[200] = function(h) {
-        var j = JSON.parse(h.responseText);
-        if (j.data) {
-            for (var i = 0; i < j.data.length; i++) {
-                add_tx_row(j.data[i], true);
-                add_tx_activity(j.data[i]);
-            }
-            drawChart();
-        }
-    }
-    h.send();
-}
-
-function tx_dategroup(tx) {
-    if (query['dategroup'] == 'year') return tx.postdate.substring(0, 4);
-    if (query['dategroup'] == 'day') return tx.postdate;
-    // Note this handles both 'month' and unknown values
-    return tx.postdate.substring(0, 7);
-}
-
-function date_from_iso(d) {
-    var y = parseInt(d.substring(0, 4));
-    var m = parseInt(d.substring(5, 7)) - 1;
-    var d = parseInt(d.substring(8, 10));
-    return new Date(y, m , d)
-}
-
-txs = {};
-
-function add_tx_row(tx, already_sorted) {
-    if (already_sorted == undefined) already_sorted = false;
-
-    var r = null;
-    var d = null;
-    var a = null;
-    var i = null;
-
-    if (tx.id == null) {
-        // Dummy transaction from a flow
-        var txid = null;
-    } else {
-        // Real transaction
-        var txid = segment(tx.id, -1);
-        txs[txid] = tx;
-    }
-
-    // Create tx row
-    var r = document.createElement("tr");
-    r.tx = tx;
-    r.tx.dategroup = tx_dategroup(tx);
-    if (txid) {
-        r.className = 'txrow';
-        r.id = 'txrow' + txid;
-        r.onclick = function () { edit_transaction(txid) };
-    } else {
-        r.className = 'flowrow';
-        if (date_from_iso(tx.postdate) < new Date()) {
-            r.style.backgroundColor = '#FFEEEE';
-        }
-    }
-
-    add_tx_cells_fast(r, txid, tx);
-
-    // Insert the row in order by postdate descending
-    var rowset = $("txheader").parentNode;
-    if (!already_sorted) {
-        for (var i=0; i < rowset.childNodes.length; i++) {
-            var existing = rowset.childNodes[i];
-            if (existing.className == 'txrow' || existing.className == 'flowrow') {
-                if (existing.tx.postdate <= tx.postdate) {
-                    rowset.insertBefore(r, existing);
-                    break;
-                }
-            }
-        }
-    }
-    // If no match:
-    if (r.parentNode == null) rowset.appendChild(r);
-
-    return r;
-}
-
-function add_tx_cells(r, txid, tx) {
-    // ID cell + link
-    var d = document.createElement("td");
-    d.className = 'txid';
-    set_text(d, txid || '');
-    r.appendChild(d);
-
-    // postdate
-    d = document.createElement("td");
-    d.className = 'txdate';
-    set_text(d, tx.postdate);
-    r.appendChild(d);
-
-    // credit
-    d = document.createElement("td");
-    d.className = 'txcredit';
-    r.appendChild(d);
-    a = document.createElement("a");
-    a.href = tx.credit;
-    set_text(a, option_text($('edit_credit').options, tx.credit));
-    d.appendChild(a);
-
-    // debit
-    d = document.createElement("td");
-    d.className = 'txdebit';
-    r.appendChild(d);
-    a = document.createElement("a");
-    a.href = tx.debit;
-    set_text(a, option_text($('edit_debit').options, tx.debit));
-    d.appendChild(a);
-
-    // description
-    d = document.createElement("td");
-    d.className = 'txdesc';
-    set_text(d, tx.description);
-    r.appendChild(d);
-
-    // amount
-    d = document.createElement("td");
-    d.className = 'txamount';
-    set_text(d, '$' + commafy(tx.amount.toFixed(2)));
-    r.appendChild(d);
-}
-
-function add_tx_cells_fast(r, txid, tx) {
-    // Like add_tx_cells, but around 10x faster
-    // using innerHTML instead of createElement and appendChild
-
-    // ID cell + link
-    var d = "<td class='txid'>" + (txid || '') + "</td>";
-    // postdate
-    d = d + "<td class='txdate'>" + tx.postdate + "</td>";
-    // credit
-    var o = option_text($('edit_credit').options, tx.credit);
-    d = d + "<td class='txcredit'><a href='" + tx.credit + "'>" + o + "</a></td>";
-    // debit
-    o = option_text($('edit_debit').options, tx.debit);
-    d = d + "<td class='txdebit'><a href='" + tx.debit + "'>" + o + "</a></td>";
-    // description
-    d = d + "<td class='txdesc'>" + tx.description + "</td>";
-    // amount
-    d = d + "<td class='txamount'>$" + commafy(tx.amount.toFixed(2)) + "</td>";
-
-    r.innerHTML = d;
-}
-
-// A list of (acct ids, dategroup columns) tuples.
-activity = [];
-activity_dategroups = [];
-
-function add_tx_activity(tx) {
-    var dg = tx_dategroup(tx);
-
-    if (activity_dategroups.indexOf(dg) == -1) {
-        activity_dategroups.push(dg);
-        // Add the dategroup to all existing account activity
-        for (var a=0; a < activity.length; a++) {
-            activity[a][1][dg] = 0;
-        }
-    }
-
-    // Credit account
-    var ca = parseInt(segment(tx.credit, -1));
-    var c_acct = accounts[ca];
-    var da = parseInt(segment(tx.debit, -1));
-    var d_acct = accounts[da];
-    for (var a = 0; a < activity.length; a++) {
-        if (activity[a][0].indexOf(ca) != -1) {
-            if (c_acct.type == 'asset' || c_acct.type == 'expense') {
-                activity[a][1][dg] -= tx.amount;
-            } else {
-                activity[a][1][dg] += tx.amount;
-            }
-        }
-        if (activity[a][0].indexOf(da) != -1) {
-            if (d_acct.type == 'asset' || d_acct.type == 'expense') {
-                activity[a][1][dg] += tx.amount;
-            } else {
-                activity[a][1][dg] -= tx.amount;
-            }
-        }
-    }
-}
-
-
-var txid_being_edited = null;
-
-function edit_transaction(txid) {
-    // Unhide any previously-edited row
-    if (!(txid_being_edited == null)) {
-        $('txrow' + txid_being_edited).style.display = 'table-row';
-    }
-
-    var txedit = $('txedit');
-
-    if (txid == null) {
-        // Keep existing values to make it easy to copy an existing row
-        // Remove the row from its current position
-        txedit.parentNode.removeChild(txedit);
-        // Insert before the header row
-        $('txheader').parentNode.insertBefore(txedit, $('txheader'));
-    } else {
-        var tx = txs[txid];
-        $('edit_postdate').value = tx.postdate;
-        $('edit_credit').value = tx.credit;
-        $('edit_debit').value = tx.debit;
-        $('edit_description').value = tx.description;
-        $('edit_amount').value = tx.amount;
-
-        // Remove the row from its current position
-        txedit.parentNode.removeChild(txedit);
-        // Insert the edit row before the existing row, then hide the existing
-        var txrow = $('txrow' + txid);
-        txrow.parentNode.insertBefore(txedit, txrow);
-        txrow.style.display = 'none';
-    }
-
-    txid_being_edited = txid;
-}
-
-function save_transaction() {
-    var tx = {};
-    tx.postdate = $('edit_postdate').value;