1. Dan Connolly
  2. quacken

Commits

Dan Connolly  committed ba18a72

start of transaction search in finjax

don't use arrays as resource items; the result is very hard to debug:
TypeError: Object #<e> has no method 'push'

  • Participants
  • Parent commits 11e0e1e
  • Branches default

Comments (0)

Files changed (5)

File finjax/finjax/__init__.py

View file
 from sqlalchemy import engine_from_config
 
 from .models import make_session
-from .views import AccountsView
+from .views import AccountsList, TransactionsQuery
+
 
 def main(global_config, **settings):
     """ This function returns a Pyramid WSGI application.
     config = Configurator(settings=settings)
     config.add_static_view('static', 'static', cache_max_age=3600)
 
-    config.add_route('accounts', '/accounts')
-    av = AccountsView(session)
-    av.config(config, 'accounts')
+    config.add_route('account', '/account/{guid}')
+    av = AccountsList(session)
+    av.config(config, 'account')
+
+    config.add_route('transaction', '/transaction/{guid}')
+    tv = TransactionsQuery(session)
+    tv.config(config, 'transaction')
 
     return config.make_wsgi_app()
 

File finjax/finjax/models.py

View file
 from sqlalchemy import (
-    Column,
-    Text,
-    String,
-    Boolean,
+    Column, ForeignKey,
+    Integer, String, Boolean,
+    Date, Text,
     )
-
+from sqlalchemy import orm
 from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
-    scoped_session,
-    sessionmaker,
-    )
-
+from sqlalchemy.orm import scoped_session, sessionmaker
 from zope.sqlalchemy import ZopeTransactionExtension
 
 SessionMaker = sessionmaker(extension=ZopeTransactionExtension())
     guid = Column(String, primary_key=True)
 
 
-class Accounts(Base, GuidMixin):
+class Account(Base, GuidMixin):
     __tablename__ = 'accounts'
     name = Column(String)
     account_type = Column(String)  # should be Enumeration...
-    parent_guid = Column(String)
+    parent_guid = Column(String, ForeignKey('accounts.guid'))
+    parent = orm.relationship('Account')
     description = Column(Text)
     hidden = Column(Boolean)
     placeholder = Column(Boolean)
+
+
+class Transaction(Base, GuidMixin):
+    __tablename__ = 'transactions'
+    currency_guid = Column(String)
+    num = Column(String)
+    post_date = Column(Date)
+    enter_date = Column(Date)
+    description = Column(String)
+    splits = orm.relationship('Split')
+
+
+class Split(Base, GuidMixin):
+    __tablename__ = 'splits'
+    tx_guid = Column(String, ForeignKey('transactions.guid'))
+    transaction = orm.relationship('Transaction')
+    account_guid = Column(String, ForeignKey('accounts.guid'))
+    account = orm.relationship('Account')
+    memo = Column(String)
+    action = Column(String)
+    reconcile_state = Column(String)
+    reconcile_date = Column(Date)
+    value_num = Column(Integer)
+    value_denom = Column(Integer)
+    # TODO: derive value, a decimal
+    quantity_num = Column(Integer)
+    quantity_denom = Column(Integer)
+    lot_guid = Column(String)
+
+
+def _fix_date(col, x):
+    if x and col.type.__class__ == Date:
+        return x.isoformat()
+    else:
+        return x
+
+def jrec(rec, cols):
+    return dict([(c.name, _fix_date(c, getattr(rec, c.name)))
+                 for c in cols])

File finjax/finjax/static/fin.js

View file
 // cribbed from http://docs.angularjs.org/#!/tutorial/step_05
 
+angular.service('Account', function($resource) {
+    return $resource('../account/:guid', {}, {
+	query: {method: 'GET', params: {guid: '-'}, isArray: true}
+    });
+});
+
+
 // todo: consider whether global controllers are fine.
-function AccountsCtrl($xhr) {
+function AccountsCtrl(Account) {
     var self = this;
 
-    $xhr('GET', '../accounts', function(code, response) {
+    self.accounts = Account.query({}, function(accounts) {
 	var a;
 	var parents = {};
 	var account_index = {}; // by guid
 	var acct;
 
-	self.accounts = response;
-
-	roots = angular.Array.filter(self.accounts,
+	roots = angular.Array.filter(accounts,
 				     function(a) {
 					 return a.account_type == 'ROOT'
 				     });
 	self.root = roots[0];  // TODO: what if there is none?
 
-	for (a = 0; a < self.accounts.length; a++) {
-	    acct = self.accounts[a];
+	for (a = 0; a < accounts.length; a++) {
+	    acct = accounts[a];
 	    account_index[acct.guid] = acct;
 	}
 	self.account_index = account_index;
 
 	self.children = function(pacct) {
 	    return angular.Array.filter(
-		self.accounts,
+		accounts,
 		function(ch) {
 		    return ch.parent_guid == pacct.guid;
 		});
 	};
     });
 }
+AccountsCtrl.$inject = ['Account'];
 
 
-AccountsCtrl.$inject = ['$xhr'];
+angular.service('Transaction', function($resource) {
+    return $resource('../transaction/:guid', {}, {
+	// override the query method to make sure the trailing / is kept
+	query2: {method: 'GET', params: {guid: '-'},
+		 isArray: true, verifyCache: true}
+    });
+});
+
+
+function TransactionsCtrl(Transaction, $log) {
+    var self = this;
+
+    self.matches = [{tx: {post_date: '2012-01-01',
+			  description: 'fun fun'},
+		     split: {memo: 'memo',
+			     amount_num: 200,
+			     amount_denom: 100}}];
+    self.search = function(qtxt) {
+	$log.info('search query text: ' + qtxt);
+	self.matches = Transaction.query2({q: qtxt}, function(res, hdrs) {
+	    $log.info('matches: ' + res.length);
+	});
+    }
+}
+TransactionsCtrl.$inject = ['Transaction', '$log'];
 
 // todo: testing

File finjax/finjax/static/index.html

View file
 <!doctype html>
 <html xmlns:ng="http://angularjs.org">
   <head>
-    <title>My GnuCash Accounts</title>
+    <title>My GnuCash Data</title>
     <meta charset="utf-8" />
+    <style type="text/css">
+      .money { text-align: right }
+    </style>
   </head>
-  <body ng:controller="AccountsCtrl">
-    <h1>Accounts</h1>
+  <body>
+    <h1>Finance Fun</h1>
+    <div ng:controller="AccountsCtrl">
+      <h2>Accounts</h2>
 
-    <table>
-      <tr><th>Account Name</th><th>Type</th></tr>
-      <tr ng:repeat="account in
-		     children(root).$filter({'hidden': false}).$orderBy('name')">
-	<td>{{account.name}}</td><td>{{account.account_type}}</td>
-      </tr>
-    </table>
+      <table>
+	<tr><th>Account Name</th><th>Type</th></tr>
+	<tr ng:repeat="account in
+		       children(root).$filter({'hidden': false}).$orderBy('name')">
+	  <td>{{account.name}}</td><td>{{account.account_type}}</td>
+	</tr>
+      </table>
+    </div>
+
+    <div ng:controller="TransactionsCtrl">
+      <h2>Transactions</h2>
+      <p><input name="q"/>
+      <button ng:click="search(q)">Search</button></p>
+      <table>
+	<tr><th>Date</th><th>Description</th><th>Memo</th>
+	<th>Amount</th></tr>
+
+	<tr ng:repeat="m in matches">
+	  <td>{{m.tx.post_date}}</td>
+	  <td>{{m.tx.description}}</td>
+	  <td>{{m.split.memo}}</td>
+	  <td class="money">{{m.split.amount_num / m.split.amount_denom }}</td>
+	</tr>
+
+      </table>
+    </div>
 
     <script src="http://code.angularjs.org/angular-0.9.12.js" ng:autobind></script>
     <script src="fin.js" type="text/javascript"></script>

File finjax/finjax/views.py

View file
 from pyramid.response import Response
 
+from sqlalchemy import or_
 from sqlalchemy.exc import DBAPIError
 
 from .models import (
-    Accounts,
+    Account, Transaction, Split,
+    jrec
     )
 
 
-class JSONView(object):
+class JSONDBView(object):
+    def __init__(self, session):
+        self._session = session
+
     def config(self, config, route_name):
         config.add_view(self, route_name=route_name, renderer='json')
 
 
-class AccountsView(JSONView):
-    def __init__(self, session):
-        self._session = session
-
+class AccountsList(JSONDBView):
     def __call__(self, request):
         try:
-            accounts = self._session.query(Accounts)
+            accounts = self._session.query(Account)
         except DBAPIError:
             return Response(conn_err_msg,
                             content_type='text/plain', status_int=500)
-        cols = Accounts.__table__.columns
+        cols = Account.__table__.columns
         return [dict([(c.name, getattr(acct, c.name)) for c in cols])
                 for acct in accounts]
 
 
+class TransactionsQuery(JSONDBView):
+    def __call__(self, request, limit=200):
+        q = request.params.get('q', None)
+        if q is None:
+            return Response('missing q param', content_type='text/plain',
+                            status_int = 400)
+
+        try:
+            qpattern = '%' + q + '%'
+            matches = self._session.query(Split).join(Transaction).filter(
+                or_(Split.memo.like(qpattern),
+                    Transaction.description.like(qpattern)))[:limit]
+        except DBAPIError:
+            return Response(conn_err_msg,
+                            content_type='text/plain', status_int=500)
+        scols = Split.__table__.columns
+        tcols = Transaction.__table__.columns
+
+        # todo: return all the splits of the relevant transactions
+        return [{'tx': jrec(split.transaction, tcols),
+                 'split': jrec(split, scols)}
+                  for split in matches]
+
+
 conn_err_msg = """\
 Pyramid is having a problem using your SQL database.  The problem
 might be caused by one of the following things: