Dan Connolly avatar Dan Connolly committed 4ddad13

checkpoint: struggling with file upload in AngularJS

Comments (0)

Files changed (6)

 ;@@@@
 ;http://code.google.com/p/sqlautocode/
 ;finquick$ sqlautocode --declarative --output=gc.py sqlite:///gc-checking.gnucash
-;sqlalchemy.url = sqlite:///%(here)s/gnc-checking.gnucash
-sqlalchemy.url = sqlite:///%(here)s/finquick.db
-bootstrap_db = true
+sqlalchemy.url = sqlite:///%(here)s/gnc-checking.gnucash
+;sqlalchemy.url = sqlite:///%(here)s/finquick.db
+;bootstrap_db = true
 
 [server:main]
 use = egg:waitress#main

finquick/models.py

         return [cls()]
 
 
-class GuidMixin(object):
+class ReprMixin(object):
+    def __repr__(self):
+        '''Represent orm objects as useful, deterministic strings.
+
+        >>> class T(Base, GuidMixin):
+        ...     __tablename__ = 'person'
+        ...     name = Column(String)
+        >>> T(guid=_n2g('Bob'), name='Bob')
+        T(guid='8b415c81c3255b6b975a40e0b5cdb699', name='Bob')
+        '''
+        cols = self.__class__.__table__.columns
+        vals = [(c.name, getattr(self, c.name))
+                for c in cols]
+        return '%s(%s)' % (self.__class__.__name__,
+                           ', '.join(['%s=%s' % (n, repr(v))
+                                     for n, v in vals]))
+
+
+class GuidMixin(ReprMixin):
     '''Provide guid primary key as used by many tables in GnuCash.
     '''
     T = String(32)
         '''
         return str(u).replace('-', '')
 
-    def __repr__(self):
-        '''Represent orm objects as useful, deterministic strings.
-
-        >>> class T(Base, GuidMixin):
-        ...     __tablename__ = 'person'
-        ...     name = Column(String)
-        >>> T(guid=_n2g('Bob'), name='Bob')
-        T(guid='8b415c81c3255b6b975a40e0b5cdb699', name='Bob')
-        '''
-        cols = self.__class__.__table__.columns
-        vals = [(c.name, getattr(self, c.name))
-                for c in cols]
-        return '%s(%s)' % (self.__class__.__name__,
-                           ', '.join(['%s=%s' % (n, repr(v))
-                                     for n, v in vals]))
-
 
 def _n2g(name):
     ns = uuid.NAMESPACE_OID  # a bit of a kludge

finquick/ofxin.py

     >>> models.Mock.sql='test/fin1_init.sql'
     >>> (session, i) = models.Mock.make([models.KSession, Importer])
     >>> from models import Account, Split
+
+    .. todo:: find account based on online_id
+
     >>> bank = session.query(Account).\
     ...        filter(Account.name == 'Checking Account').one()
     >>> [split.value_num
     >>> i.prepare(summary, transactions, bank)
 
     Then import the unmatched transactions and note the results:
-    >>> i.execute(bank, exp, summary['curdef'])
+    >>> [tx.fitid for tx in  i.execute(bank, exp, summary['curdef'])]
+    [u'2-6', u'2-9']
     >>> [split.value_num
     ...  for split in session.query(Split).filter(Split.account == bank)]
     [-6000, -6000]
     Import is idempotent; doing it again has no effect:
     >>> i.prepare(summary, transactions, bank)
     >>> i.execute(bank, exp, summary['curdef'])
+    []
     >>> [split.value_num
     ...  for split in session.query(Split).filter(Split.account == bank)]
     [-6000, -6000]
             select().\
             where(and_(tsl.c.name == 'online_id',
                        ta.c.guid == acct.guid))
-        matches = tst.join(online_ids, tst.c.fitid == tsl.c.string_val).\
+        match_q = tst.join(online_ids, tst.c.fitid == tsl.c.string_val).\
             select().with_only_columns([tst.c.fitid, tsl.c.obj_guid])
-        for fitid, obj_guid in session.execute(matches).fetchall():
+        matches = session.execute(match_q).fetchall()
+        for fitid, obj_guid in matches:
             session.execute(tst.update().values(match_guid=obj_guid).\
                                 where(tst.c.fitid == fitid))
+        return matches
 
     def execute(self, acct, txfr, currency):
         session = self._ds()  # hmm... instantiate this?
             select guid from commodities
             where namespace='CURRENCY' and mnemonic = :currency''',
                                         dict(currency=currency)).fetchone()[0]
-        for ofxtx in session.query(StmtTrn).\
-                filter(StmtTrn.match_guid == None).all():
+        novel = session.query(StmtTrn).\
+                filter(StmtTrn.match_guid == None).all()
+        for ofxtx in novel:
             fmt = models.GuidMixin.fmt
             tx = models.Transaction(guid=fmt(self._uuidgen()),
                                     currency_guid=currency_guid,
             session.add(online_id)
             session.add(s2)
         session.flush()
+        return novel
 
 
-class StmtTrn(models.Base):
+class StmtTrn(models.Base, models.ReprMixin):
     '''Temporary table for OFX import.
+
+    .. todo:: detect lost updates by using dtserver or some such.
     '''
     __tablename__ = 'ofx_stmttrn'
     fitid = Column(String(80), primary_key=True)

finquick/static/accounts.html

       </tbody>
     </table>
 
+    <div><h2>OFX Upload</h2>
+    <select name="account_guid">
+      <option id="selected_account" value="{{selected_account.guid}}"
+	      >{{selected_account.name}}</option>
+    </select>
+    <p><label>OFX file:
+
+    <ng:chooser choice="ofx_file" update="note_ofx_file(ofx_file)" />
+    <input type="file" name="ofx_file" />
+    </label></p>
+    <button ng:click='prepare()' ng-disabled='{{! ofx_file}}'
+	    >Prepare</button>
+    <button ng:click='execute()' ng-disabled='{{true}}'
+	    >Execute</button>
+    </div>
+
     <hr />
     <address>
       <a href="https://bitbucket.org/DanC/finquick">FinQuick</a><br />

finquick/static/fin.js

 	}
 	parent.expanded = expanded;
 	$log.info('toggle: ' + parent.name + ' to: ' + expanded);
+	self.selected_account = parent;  // for file upload. kludge?
     };
 
     self.visibility = function(acct) {
 	}
 	return s;
     });
+
+    self.selected_account = null;
+    self.ofx_file = null;
+    self.ofx_summary = null;
+    self.note_ofx_file = function(elt) {
+	alert(elt.value);
+    }
+    self.prepare = function() {
+	// http://www.w3.org/TR/FileAPI/
+	var reader = new FileReader();
+	reader.readAsText(self.ofx_file, 'utf-8'); // blob? win 1252?
+	reader.onerror = function() {
+	    alert('LOSE! @@');
+	};
+	reader.onload = function(evt) {
+	    var content = evt.target.result;
+	    alert('WIN!: ' + content.substr(1, 20));
+	    // @@... call prepare...
+	};
+    }
 }
 AccountsCtrl.$inject = ['Account', 'AccountSummary', '$log'];
 
+/* based on
+File upload - how to / examples?
+Oct 2011
+https://groups.google.com/group/angular/browse_thread/thread/334a155cbc886c92/bcb5b998f0fac10f?lnk=gst&q=file+upload#bcb5b998f0fac10f
+http://jsfiddle.net/vojtajina/epCyK/a
+*/
+angular.widget('ng:chooser', function(elm) {
+    var choice = elm.attr('choice'), update=elm.attr('update');
+
+    console.log(choice);
+    return function() {
+	var scope = this;
+	this.$watch(choice, function(newV) {
+	    console.log(newV);
+	    if (newV != Math.NaN) {
+		scope.$eval(update);
+	    }
+	});
+    };
+});
 
 angular.service('Transaction', function($resource) {
     return $resource('../transaction/:guid', {}, {

finquick/views.py

 '''
 
 from itertools import groupby
+import json
 
 from injector import inject
 from pyramid.config import Configurator
 from pyramid.response import Response
 from sqlalchemy.exc import DBAPIError
 
+import ofxin
 from dotdict import dotdict
 from models import (
     Account, Transaction,
                       content_type='text/plain', status_int=500)
 
 
+class OFXUpload(object):
+    template = ''
+
+    @inject(importer=ofxin.Importer, session=KSession)
+    def __init__(self, importer, session):
+        self._importer = importer
+        self._session = session
+
+    def config(self, config, route_name):
+        config.add_view(self, route_name=route_name, renderer='json')
+
+    def __call__(self, request):
+        data = json.load(request.body_file)  # TODO: catch errors
+        if ('ofx_data' in data
+            and 'account_guid' in data):
+            return self.prepare(data['ofx_data'],
+                         data['account_guid'])
+        elif ('account_guid' in data
+              and 'transfer_guid' in data):
+            return self.execute(data['account_guid'],
+                                data['transfer_guid'])
+        else:
+            return Response('ofx_data file, account_guid required to prepare'
+                            '; account_guid, transfer_guid required to execute',
+                            content_type='text/plain',
+                            status_int = 400)
+
+    def prepare(self, ofxin, account_guid):
+        summary, transactions = ofxin.OFXParser.ofx_data(ofxin)
+        account = self._session.query(Account).\
+            filter(Account.guid == account_guid).one()
+        matches = self._importer.prepare(summary, transactions, account)
+        alltx = self._session.query(ofxin.StmtTrn).all()
+        return dict(summary=summary,
+                    transactions=alltx,
+                    matches=matches)
+
+    def execute(self, account_guid, transfer_guid, currency):
+        account = self._session.query(Account).\
+            filter(Account.guid == account_guid).one()
+        transfer = self._session.query(Account).\
+            filter(Account.guid == transfer_guid).one()
+        novel = self._importer.execute(account, transfer, currency)
+        return dict(account=account,
+                    transfer=transfer,
+                    transactions=novel)
+
+
 class FinquickAPI(object):
     '''Finquick REST/JSON API configuration.
 
     account_route = dotdict(name='account', path='/account/{guid}')
     summary_route = dotdict(name='summary', path='/accountSummary')
     transaction_route = dotdict(name='transaction', path='/transaction/{guid}')
+    ofx_route = dotdict(name='ofx_import', path='/ofx_import')
 
     @inject(config=Configurator,
             av=AccountsList,
             sv=AccountSummary,
+            ofxv=OFXUpload,
             tqv=TransactionsQuery)
-    def __init__(self, config, av, sv, tqv):
+    def __init__(self, config, av, sv, ofxv, tqv):
         self._config = config
         self._account_view = av
         self._summary_view = sv
+        self._ofx_view = ofxv
         self._transaction_view = tqv
 
     def add_rest_api(self):
         for (rt, view) in (
             (self.account_route, self._account_view),
             (self.summary_route, self._summary_view),
+            (self.ofx_route, self._ofx_view),
             (self.transaction_route, self._transaction_view)):
             self._config.add_route(rt.name, rt.path)
             view.config(self._config, rt.name)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.