dabo Commit
Revision 4547
Date: 2008-10-04 08:55:23 -0700 (Sat, 04 Oct 2008)
Author: Ed
Trac: http://svn.dabodev.com/trac/dabo/changeset/4547

Changed:
A   trunk/dabo/biz/RemoteBizobj.py
U   trunk/dabo/biz/__init__.py
U   trunk/dabo/biz/dBizobj.py
U   trunk/dabo/dApp.py
U   trunk/dabo/dException.py
U   trunk/dabo/db/dCursorMixin.py
A   trunk/dabo/db/dbWeb.py
A   trunk/dabo/lib/RemoteConnector.py
U   trunk/dabo/lib/__init__.py
U   trunk/dabo/lib/connParser.py
A   trunk/dabo/lib/dejavuJSON.py
A   trunk/dabo/lib/manifest.py

Log:
These are the changes to the framework required to create web-enabled 
applications in Dabo.

There should be no effect on any existing application, but please test 
thoroughly, and report any issues. I have tested every app of mine over the 
past several weeks without incident.

Web-enabled apps will require that the simplejson module for Python be 
installed. If you do not create a web app, you do not need to install this 
module.

The server side of this is just about ready, and will be committed soon. The 
tools for creation of web apps are still on the drawing board.


Diff:
Added: trunk/dabo/biz/RemoteBizobj.py
===================================================================
--- trunk/dabo/biz/RemoteBizobj.py                              (rev 0)
+++ trunk/dabo/biz/RemoteBizobj.py      2008-10-04 15:55:23 UTC (rev 4547)
@@ -0,0 +1,213 @@
+import pickle
+import os
+import time
+
+import dabo
+from dabo.dLocalize import _
+import dabo.dConstants as kons
+from dabo.lib.connParser import importConnections
+import dabo.dException as dException
+from dBizobj import dBizobj
+
+
+
+def getCacheDB():
+       curr = os.getcwd()
+       db = os.path.join(curr, "DaboBizCache.db")
+       cxn = dabo.db.dConnection(connectInfo={"DbType": "SQLite", "Database": 
db})
+       cursor = cxn.getDaboCursor()
+       return cursor
+
+
+class RemoteBizobj(dBizobj):
+       def _beforeInit(self):
+               crs = getCacheDB()
+               sql = """create table if not exists bizcache (
+                       hashval text, 
+                       updated int,
+                       keyfield text,
+                       pickledata text)
+                       """
+               crs.execute(sql)
+               return super(RemoteBizobj, self)._beforeInit()
+
+
+       def _afterInit(self):
+               # This is used as the identifier that connects to the client 
bizobj
+               self.hashval = None
+               self.defineConnection()
+               super(RemoteBizobj, self)._afterInit()
+
+
+       def defineConnection(self):
+               """You must define and create the connection in this method. 
Otherwise
+               an error will be raised. Pass the connection information to 
setConnectionParams();
+               if you are using a .cnxml file, pass that in the 'cxnfile' 
parameter; otherwise, use
+               the various connection setting params to define the connection.
+               NOTE: You must use a SINGLE-CONNECTION .cnxml file; if multiple 
connections
+               are defined, there is no way for the bizobj to know which one 
you want to use.
+               """
+               # Force an override in child bizobjs
+               raise NotImplementedError
+
+
+       @classmethod
+       def load(cls, hashval, ds):
+               crs = getCacheDB()
+               biz = cls()
+               biz.DataSource = ds
+               biz.hashval = hashval
+               sql = "select * from bizcache where hashval = ?"
+               crs.execute(sql, (hashval, ))
+               if crs.RowCount:
+                       biz.KeyField = crs.Record.keyfield
+                       # Unpickle
+                       crsData = pickle.loads(crs.Record.pickledata)
+                       # This is a dict with cursor keys as the keys, and 
+                       # values as a (dataset, typedef) tuple.
+                       for kk, (ds, typinfo) in crsData.items():
+                               tmpCursor = biz.createCursor(key=kk)
+                               tmpCursor._storeData(ds, typinfo)
+               return biz
+
+
+       def setConnectionParams(self, cxnfile=None, dbType=None, database=None, 
+                       host=None, user=None, password=None, 
plainTextPassword=None):
+               if cxnfile:
+                       cxDict = importConnections(cxnfile)
+                       self.setConnection(cxDict.values()[0])
+               else:
+                       cxnDict = {"DbType": dbType, "Database": database}
+                       if host:
+                               cxnDict["Host"]  = host
+                       if user:
+                               cxnDict["User"] = user
+                       if plainTextPassword:
+                               cxnDict["PlainTextPassword"] = plainTextPassword
+                       if password:
+                               cxnDict["Password"] = password
+                       ci = dabo.db.dConnectInfo(cxnDict)
+                       cn = dabo.db.dConnection(ci)
+                       self.setConnection(cn)
+
+
+       def storeToCache(self, hashval):
+               """Store data info to the cache for the next time the same 
bizobj
+               is needed.
+               """
+               crs = getCacheDB()
+               sql = """delete from bizcache where hashval = ?"""
+               crs.execute(sql, (hashval,))
+               sql = """insert into bizcache (hashval, updated, keyfield, 
pickledata)
+                       values (?, ?, ?, ?)"""
+               updated = int(time.time())
+               pd = {}
+               cursorDict = self._cursorDictReference()
+               for kk, cursor in cursorDict.items():
+                       pd[kk] = (cursor.getDataSet(returnInternals=True), 
cursor.getDataTypes())
+               pklData = pickle.dumps(pd, pickle.HIGHEST_PROTOCOL)
+               print
+               print "PKTYP", type(pklData)
+               crs.execute(sql, (hashval, updated, self.KeyField, pklData))
+
+
+       def storeRemoteSQL(self, sql):
+               """The web backend uses '~~' as the name enclosure character. 
Convert that
+               to the correct character for the actual backend.
+               """
+               remoteChar = "~~"
+               localChar = self._CurrentCursor.BackendObject.nameEnclosureChar
+               self.UserSQL = sql.replace(remoteChar, localChar)
+
+
+       def applyDiffAndSave(self, diff, primary=False):
+               """Diffs are dicts in the format:
+                       {hashval: (DataSource, KeyField, [changes])}
+               where 'changes' is a list of dicts; one for each changed row in
+               the bizobj. Each row dict has the following key/value pairs:
+                       KeyField: pk value
+                       ColumnName: (origVal, newVal)
+                       Column2Name: (origVal, newVal)
+                       ...
+               The 'diff' dict we receive can have 1 or two keys. One that 
will always
+               be present is the hashval for this bizobj. If this bizobj has 
related child
+               bizobjs, and they have changes, there will be a 'children' key 
that will
+               contain a list of one diff for each child bizobj with changes.
+               
+               If this is the primary bizobj called from the web server, the 
'primary'
+               parameter will be true, meaning that this bizobj will handle 
transactions.
+               """
+               if primary:
+                       self._CurrentCursor.beginTransaction()
+               myDiff = diff.pop(self.hashval, None)
+               if myDiff:
+                       self.DataSource = myDiff[0]
+                       self.KeyField = kf = myDiff[1]
+                       changeRecs = myDiff[2].get(self.hashval, [])
+                       for rec in changeRecs:
+                               newrec = rec.get(kons.CURSOR_TMPKEY_FIELD, 
False)
+                               if newrec:
+                                       self.new()
+                               else:
+                                       pk = rec.get(kf)
+                                       try:
+                                               self.moveToPK(pk)
+                                       except dException.RowNotFoundException:
+                                               raise 
dException.WebServerException, _("PK '%s' not present in dataset for DataSource 
'%s'") % (pk, self.DataSource)
+                               for col, vals in rec.items():
+                                       if col in (kf, 
kons.CURSOR_TMPKEY_FIELD):
+                                               continue
+                                       oldval, newval = vals
+                                       if not newrec:
+                                               # Check for update conflicts; 
abort if found
+                                               currval = self.getFieldVal(col)
+                                               if currval != oldval:
+                                                       raise 
dException.WebServerException, _("Update Conflict: the value in column '%s' has 
been changed by someone else.") % col
+                                       self.setFieldVal(col, newval)
+
+                       kids = diff.pop("children", None)
+                       if kids:
+                               for kid in kids:
+                                       for kk, vv in kid.items:
+                                               # The key will either be the 
kid's hashval, or the string 'children'.
+                                               # Skip 'children', as the child 
bizobj will process that.
+                                               if kk == "children":
+                                                       continue
+                                               kidHash = kk
+                                               kidDS = vv[0]
+                                               kidBiz = 
RemoteBizobj.load(kidHash, kidDS)
+                                               kidBiz.applyDiffAndSave(kid)
+
+                       try:
+                               self.saveAll()
+                       except dException.ConnectionLostException, e:
+                               if primary:
+                                       
self._CurrentCursor.rollbackTransaction()
+                                       return (500, _("Connection to database 
was lost."))
+                               else:
+                                       raise
+                       except dException.NoRecordsException, e:
+                               if primary:
+                                       
self._CurrentCursor.rollbackTransaction()
+                                       return (204, _("No records were 
saved."))
+                               else:
+                                       raise
+                       except dException.BusinessRuleViolation, e:
+                               if primary:
+                                       
self._CurrentCursor.rollbackTransaction()
+                                       return (409, _("Business Rule 
Violation: %s.") % e)
+                               else:
+                                       raise
+                       except dException.DBQueryException, e:
+                               if primary:
+                                       
self._CurrentCursor.rollbackTransaction()
+                                       return (400, _("Database Query 
Exception: %s.") % e)
+                               else:
+                                       raise
+               if primary:
+                       self._CurrentCursor.commitTransaction()
+
+
+       def beforeDelete(self):
+               # As this is on the server side, we don't wan't to ask 'Are you 
sure?'
+               pass

Modified: trunk/dabo/biz/__init__.py
===================================================================
--- trunk/dabo/biz/__init__.py  2008-10-04 15:21:32 UTC (rev 4546)
+++ trunk/dabo/biz/__init__.py  2008-10-04 15:55:23 UTC (rev 4547)
@@ -20,6 +20,8 @@
 
 """
 from dBizobj import dBizobj
+from RemoteBizobj import RemoteBizobj
+
 from dAutoBizobj import dAutoBizobj
 from dAutoBizobj import autoCreateTables
 from dAutoBizobj import autoCreateTables

Modified: trunk/dabo/biz/dBizobj.py
===================================================================
--- trunk/dabo/biz/dBizobj.py   2008-10-04 15:21:32 UTC (rev 4546)
+++ trunk/dabo/biz/dBizobj.py   2008-10-04 15:55:23 UTC (rev 4547)
@@ -8,8 +8,8 @@
 from dabo.dLocalize import _
 import dabo.dException as dException
 from dabo.dObject import dObject
+from dabo.lib.RemoteConnector import _RemoteConnector as remote
 
-
 NO_RECORDS_PK = "75426755-2f32-4d3d-86b6-9e2a1ec47f2c"  ## Can't use None
 
 
@@ -111,6 +111,10 @@
                        self.createCursor()
 
 
+       def _getConnection(self):
+               return self._connection
+
+
        def getTempCursor(self):
                """Occasionally it is useful to be able to run ad-hoc queries 
against
                the database. For these queries, where the results are not 
meant to
@@ -171,6 +175,7 @@
                        crs.requery()
                        self.first()
                self.afterCreateCursor(crs)
+               return crs
 
 
        def _getCursorClass(self, main, secondary):
@@ -261,6 +266,7 @@
                self.afterLast()
 
 
+       @remote
        def beginTransaction(self):
                """Attempts to begin a transaction at the database level, and 
returns
                True/False depending on its success. 
@@ -271,6 +277,7 @@
                return ret
 
 
+       @remote
        def commitTransaction(self):
                """Attempts to commit a transaction at the database level, and 
returns
                True/False depending on its success. 
@@ -281,6 +288,7 @@
                return ret
 
 
+       @remote
        def rollbackTransaction(self):
                """Attempts to rollback a transaction at the database level, 
and returns
                True/False depending on its success. 
@@ -327,6 +335,7 @@
                                del(dabo._bizTransactionToken)
 
 
+       @remote
        def saveAll(self, startTransaction=True):
                """Saves all changes to the bizobj and children."""
                cursor = self._CurrentCursor
@@ -362,6 +371,7 @@
                                dabo.errorLog.write(_("Failed to set RowNumber. 
Error: %s") % e)
 
 
+       @remote
        def save(self, startTransaction=True):
                """Save any changes that have been made in the current row.
 
@@ -485,6 +495,7 @@
                self.afterDeleteAllChildren()
 
 
+       @remote
        def delete(self, startTransaction=True, inLoop=False):
                """Delete the current row of the data set."""
                cursor = self._CurrentCursor
@@ -575,6 +586,24 @@
                return self._CurrentCursor.executeSafe(sql, params)
 
 
+       def getDataDiff(self, allRows=False):
+               """Get a dict that is keyed on the hash value of this bizobj, 
with the value
+               being  a list of record changes. Default behavior is to only 
consider the
+               current row; you can change that by passing allRows=True. Each 
changed 
+               row will be present in the diff, with its PK and any columns 
whose values
+               have changed. If there are any related child bizobjs, their 
diffs will be
+               added to the dict under the key 'children' so that they can be 
processed
+               accordingly.
+               """
+               diff = {hash(self): 
self._CurrentCursor.getDataDiff(allRows=allRows)}
+               kids = []
+               for child in self.__children:
+                       kids.append(child.getDataDiff(allRows=True))
+               if kids:
+                       diff["children"] = kids
+               return diff
+
+
        def getChangedRows(self, includeNewUnchanged=False):
                """ Returns a list of row numbers for which isChanged() returns 
True. The
                changes may therefore not be in the record itself, but in a 
dependent child
@@ -786,6 +815,7 @@
                        self.UserSQL = sql
 
 
+       @remote
        def requery(self):
                """ Requery the data set.
 
@@ -1278,7 +1308,7 @@
                if self.KeyField is None:
                        raise dException.dException, _("No key field defined 
for table: ") + self.DataSource
                cc = self._CurrentCursor
-               return cc.getFieldVal(cc.KeyField)
+               return cc.getFieldVal(self.KeyField)
 
 
        def getParentPK(self):
@@ -1392,7 +1422,7 @@
                callback(self._rowTemplate % ("%s%s" % (xml, kidXML)), level)
 
 
-       def getDataSet(self, flds=(), rowStart=0, rows=None):
+       def getDataSet(self, flds=(), rowStart=0, rows=None, 
returnInternals=False):
                """ Get the entire data set encapsulated in a list.
 
                If the optional 'flds' parameter is given, the result set will 
be filtered
@@ -1402,10 +1432,22 @@
                ret = None
                cc = self._CurrentCursor
                if cc is not None:
-                       ret = self._CurrentCursor.getDataSet(flds, rowStart, 
rows)
+                       ret = self._CurrentCursor.getDataSet(flds, rowStart, 
rows, returnInternals=returnInternals)
                return ret
 
 
+       def getDataTypes(self):
+               """Returns the field type definitions as set in the cursor."""
+               return self._CurrentCursor.getDataTypes()
+
+
+       def _storeData(self, data, typs):
+               """Accepts a data set and type defintion dict, and updates the 
cursor
+               with these values.
+               """
+               self._CurrentCursor._storeData(data, typs)
+
+
        def getDataStructure(self):
                """ Gets the structure of the DataSource table. Returns a list
                of 3-tuples, where the 3-tuple's elements are:
@@ -1646,11 +1688,13 @@
 with the afterSave() hook which only gets called after a save(), and the 
 afterDelete() which is only called after a delete().""")       
 
+
        def afterCreateCursor(self, crs):
                """This hook is called after the underlying cursor object is 
created.
                The crs argument will contain the reference to the newly-created
                cursor."""
 
+
        def _syncWithCursors(self):
                """Many bizobj properties need to be passed through to the 
cursors
                that provide it with data connectivity. This method ensures 
that all
@@ -1672,8 +1716,17 @@
                crs.Encoding = self.Encoding
                crs.KeyField = self._keyField
                crs.setNonUpdateFields(self._nonUpdateFields)
-       
 
+
+       def _cursorDictReference(self):
+               """In rare situations, bizobj subclasses may need to reference 
the 
+               internal __cursors attribute. This provides a way to do that, 
but 
+               it should be stressed that this is potentially dangerous and 
could 
+               lead to lost data if not handled correctly.
+               """
+               return self.__cursors
+
+
        ## Property getter/setter methods ##
        def _getAutoPopulatePK(self):
                try:

Modified: trunk/dabo/dApp.py
===================================================================
--- trunk/dabo/dApp.py  2008-10-04 15:21:32 UTC (rev 4546)
+++ trunk/dabo/dApp.py  2008-10-04 15:55:23 UTC (rev 4547)
@@ -22,8 +22,10 @@
 from dabo.lib.SimpleCrypt import SimpleCrypt
 from dabo.dObject import dObject
 from dabo import dUserSettingProvider
+from dabo.lib.RemoteConnector import _RemoteConnector as remote
 
 
+
 class Collection(list):
        """ Collection : Base class for the various collection
        classes used in the app object.
@@ -224,7 +226,7 @@
                self.default_form = None
                # Dict of "Last-Modified" values for dynamic web resources
                self._sourceLastModified = {}
-
+               
                # For simple UI apps, this allows the app object to be created
                # and started in one step. It also suppresses the display of
                # the main form.
@@ -232,6 +234,11 @@
                        self.showMainFormOnStart = False
                        self.setup()
 
+               self._initDB()
+
+               # If running as a web app, sync the files
+               self.syncFiles()
+
                self._afterInit()
                self.autoBindEvents()
                
@@ -267,7 +274,7 @@
 
                self._initModuleNames()
                self._initDB()
-               
+
                if initUI:
                        self._initUI()
                        if self.UI is not None:
@@ -656,6 +663,44 @@
                        dabo.infoLog.write(_("File %s updated") % pth)
 
 
+       def updateFromSource(self, fileOrFiles):
+               """This method takes either a single file path or a list of 
paths, and if there
+               is a SourceURL set, checks the source to see if there are newer 
versions available,
+               and if so, downloads them.
+               """
+               if not self.SourceURL:
+                       # Nothing to do
+                       return
+               if isinstance(fileOrFiles, (list, tuple)):
+                       for f in fileOrFiles:
+                               self.updateFromSource(f)
+                       return
+               pth = fileOrFiles
+               cwd = os.getcwd()
+               # The srcFile has an absolute path; the URLs work on relative.
+               try:
+                       splt = srcFile.split(cwd)[1].lstrip("/")
+               except IndexError:
+                       splt = srcFile
+               app.urlFetch(splt)
+               try:
+                       nm, ext = os.path.splitext(splt)
+               except ValueError:
+                       # No extension; skip it
+                       nm = ext = ""
+               if ext == ".cdxml":
+                       # There might be an associated code file. If not, the 
error
+                       # will be caught in the app method, and no harm will be 
done.
+                       codefile = "%s-code.py" % nm
+                       app.urlFetch(codefile)
+
+
+       @remote
+       def syncFiles(self):
+               # Currently only used in web mode
+               pass
+
+
        def getUserSettingKeys(self, spec):
                """Return a list of all keys underneath <spec> in the user 
settings table.
                

Modified: trunk/dabo/dException.py
===================================================================
--- trunk/dabo/dException.py    2008-10-04 15:21:32 UTC (rev 4546)
+++ trunk/dabo/dException.py    2008-10-04 15:55:23 UTC (rev 4547)
@@ -68,7 +68,9 @@
        pass
 
 class DBQueryException(DatabaseException):
-       def __init__(self, err, sql):
+       def __init__(self, err, sql=None):
+               if sql is None:
+                       sql = ""
                self.sql = sql
                self.err_desc = "%s" % err
                
@@ -77,3 +79,6 @@
 
 class XmlException(dException):
        pass
+
+class WebServerException(dException):
+       pass

Modified: trunk/dabo/db/dCursorMixin.py
===================================================================
--- trunk/dabo/db/dCursorMixin.py       2008-10-04 15:21:32 UTC (rev 4546)
+++ trunk/dabo/db/dCursorMixin.py       2008-10-04 15:55:23 UTC (rev 4547)
@@ -990,29 +990,39 @@
                        rec[fld] = val
 
 
-       def getRecordStatus(self, row=None):
+       def getRecordStatus(self, row=None, pk=None):
                """ Returns a dictionary containing an element for each changed
                field in the specified record (or the current record if none is 
specified).
                The field name is the key for each element; the value is a 
2-element
                tuple, with the first element being the original value, and the 
second
-               being the current value.
+               being the current value. You can specify the record by either 
the 
+               row number or the PK.
                """
                ret = {}
-               if row is None:
-                       row = self.RowNumber
+               if pk is not None:
+                       recs = [r for r in self._records
+                                       if r[self._keyField] == pk]
+                       try:
+                               rec = recs[0]
+                       except IndexError:
+                               return ret
+               else:
+                       if row is None:
+                               row = self.RowNumber
+                       rec = self._records[row]
+                       pk = self.pkExpression(rec)
 
-               rec = self._records[row]
-               recKey = self.pkExpression(rec)
-               mem = self._mementos.get(recKey, {})
+               mem = self._mementos.get(pk, {})
 
                for k, v in mem.items():
                        ret[k] = (v, rec[k])
                return ret
 
 
-       def _getNewRecordDiff(self, row=None):
+       def _getNewRecordDiff(self, row=None, pk=None):
                """ Returns a dictionary containing an element for each field
-               in the specified record (or the current record if none is 
specified).
+               in the specified record (or the current record if none is 
specified). You
+               may specify the record by either row number or PK value.
                The field name is the key for each element; the value is a 
2-element
                tuple, with the first element being the original value, and the 
second
                being the current value.
@@ -1020,10 +1030,19 @@
                This is used internally in __saverow, and only applies to new 
records.
                """
                ret = {}
-               if row is None:
-                       row = self.RowNumber
+               if pk is not None:
+                       recs = [r for r in self._records
+                                       if r[self._keyField] == pk]
+                       try:
+                               rec = recs[0]
+                       except IndexError:
+                               return ret
+               else:
+                       if row is None:
+                               row = self.RowNumber
+                       rec = self._records[row]
+                       pk = self.pkExpression(rec)
 
-               rec = self._records[row]
                for k, v in rec.items():
                        if k not in (kons.CURSOR_TMPKEY_FIELD,):
                                ret[k] = (None, v)
@@ -1066,12 +1085,41 @@
                                if not flds and not returnInternals:
                                        # user didn't specify explicit fields 
and doesn't want internals
                                        for internal in internals:
-                                               if tmprec.has_key(internal):
-                                                       del tmprec[internal]
+                                               tmprec.pop(internal, None)
                                ds.append(tmprec)
                return dDataSet(ds)
 
 
+       def getDataTypes(self):
+               """Returns the internal _types dict."""
+               return self._types
+
+
+       def _storeData(self, data, typs):
+               """Accepts a dataset and type dict from an external source and
+               uses it as its own.
+               """
+               # clear mementos and new record flags:
+               self._mementos = {}
+               self._newRecords = {}
+               # If None is passed as the data, exit after resetting the flags
+               if data is None:
+                       return
+               # Store the values
+               self._records = data
+               self._types = typs
+               # Need to do this here to avoid lags later on.
+               self._getDataStructure()
+               # Clear the unsorted list, and then apply the current sort
+               self.__unsortedRows = []
+               if self.sortColumn:
+                       try:
+                               self.sort(self.sortColumn, self.sortOrder)
+                       except dException.NoRecordsException, e:
+                               # No big deal
+                               pass
+
+
        def replace(self, field, valOrExpr, scope=None):
                """Replaces the value of the specified field with the given 
value
                or expression. All records matching the scope are affected; if
@@ -1320,6 +1368,31 @@
                        pass
 
 
+       def getDataDiff(self, allRows=False):
+               """Create a compact representation of all the modified records
+               for this cursor.
+               """
+               diff = []
+               def rowDiff(pk):
+                       newrec = pk in self._newRecords
+                       if newrec:
+                               ret = self._getNewRecordDiff(pk=pk)
+                       else:
+                               ret = self.getRecordStatus(pk=pk)
+                       ret[self._keyField] = pk
+                       ret[kons.CURSOR_TMPKEY_FIELD] = newrec
+                       return ret
+                       
+               if allRows:
+                       for pk in self._mementos.keys():
+                               diff.append(rowDiff(pk))
+               else:
+                       pk = self.getPK()
+                       if pk in self._mementos:
+                               diff.append(rowDiff(pk))
+               return diff
+
+
        def pregenPK(self):
                """Various backend databases require that you manually
                generate new PKs if you need to refer to their values afterward.

Added: trunk/dabo/db/dbWeb.py
===================================================================
--- trunk/dabo/db/dbWeb.py                              (rev 0)
+++ trunk/dabo/db/dbWeb.py      2008-10-04 15:55:23 UTC (rev 4547)
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import datetime
+from dabo.dLocalize import _
+from dBackend import dBackend
+from dbSQLite import SQLite
+from dabo.lib.RemoteConnector import _RemoteConnector as remote
+
+
+
+class Web(SQLite):
+       def __init__(self):
+               SQLite.__init__(self)
+               self.nameEnclosureChar = "~~"
+
+
+       def getConnection(self, connectInfo, **kwargs):
+               connectInfo.Database = ":memory:"
+               self._connection = super(Web, self).getConnection(connectInfo, 
**kwargs)
+               return self._connection
+
+
+       @remote
+       def getTables(self, includeSystemTables=False):
+               raise ValueError, _("Table listing must come from web service")
+               
+
+       def getTableRecordCount(self, tableName):
+               raise ValueError, _("Record Count must come from web service")
+
+
+       @remote
+       def getFields(self, tableName, crs=None):
+               raise ValueError, _("Field listing must come from web service")
+
+
+       def _getRemoteURL(self):
+               return self._connection.connectInfo.RemoteHost
+

Added: trunk/dabo/lib/RemoteConnector.py
===================================================================
--- trunk/dabo/lib/RemoteConnector.py                           (rev 0)
+++ trunk/dabo/lib/RemoteConnector.py   2008-10-04 15:55:23 UTC (rev 4547)
@@ -0,0 +1,317 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import urllib
+import urllib2
+import urlparse
+import sys
+import os
+import re
+import pickle
+from os.path import join as pathjoin
+from zipfile import ZipFile
+from cStringIO import StringIO
+
+import dabo
+import dabo.dException as dException
+from dabo.dObject import dObject
+from dabo.dLocalize import _
+from dabo.lib.manifest import Manifest
+
+
+
+class _RemoteConnector(object):
+       """This class handles all of the methods that will need to be carried 
out on 
+       the server instead of locally.
+       """
+       def __init__(self, fn):
+               self.fn = fn
+               self.fname = fn.__name__
+               self._authHandler = None
+               self._urlOpener = None
+               super(_RemoteConnector, self).__init__()
+
+
+       def __call__(self, *args, **kwargs):
+               remote = bool(self.UrlBase)
+               if remote:
+                       try:
+                               decFunc = getattr(self, self.fname, None)
+                               if decFunc:
+                                       return decFunc(*args, **kwargs)
+                               else:
+                                       # Generic remote method
+                                       url = self._getFullUrl()
+                                       return self._read(url)
+                       except urllib2.URLError, e:
+                               try:
+                                       code, txt = e.reason
+                               except AttributeError:
+                                       code = e.code
+                               if code == 500:
+                                       # Internal server error
+                                       dabo.errorLog.write(_("500 Internal 
Server Error received"))
+                                       return
+                               elif code == 61:
+                                       # Connection refused; server is down
+                                       dabo.errorLog.write(_("\n\nThe remote 
server is not responding; exiting.\n\n"))
+                                       # This could certainly be improved with 
a timeout loop instead of instantly quitting.
+                                       sys.exit()
+                               else:
+                                       # Some other proble; raise the error so 
the developer can debug
+                                       raise
+               return self.fn(self.obj, *args, **kwargs)
+
+
+       def __get__(descr, inst, instCls=None):
+               descr.obj = inst
+               return descr
+
+
+       def _join(self, pth):
+               """Joins the path to the class's UrlBase to create a new URL."""
+               return pathjoin(self.UrlBase, pth)
+
+
+       def _getFullUrl(self, *args):
+               ret = pathjoin(self.UrlBase, "bizservers", "biz", "%s" % 
hash(self.obj), self.obj.DataSource, self.fname, *args)
+               return ret
+
+
+       def _getManifestUrl(self, *args):
+               ret = pathjoin(self.UrlBase, "manifest", *args)
+               return ret
+
+
+       def _read(self, url, params=None):
+               if params:
+                       prm = urllib.urlencode(params)
+               else:
+                       prm = None
+               try:
+                       res = self.UrlOpener.open(url, data=prm)
+               except urllib2.HTTPError, e:
+                       print "ERR %s" % e
+                       return None
+               ret = res.read()
+               return ret
+
+
+       def _storeEncodedDataSet(self, enc):    
+               data, typs = dabo.lib.jsonDecode(enc)
+               # 'typs' is pickled, as it has actual Python types, and cannot 
be JSON encoded.
+               typs = pickle.loads(typs)
+               self.obj._storeData(data, typs)
+
+
+       def requery(self):
+               biz = self.obj
+               url = self._getFullUrl()
+               sql = biz.getSQL()
+               # Get rid of as much unnecessary formatting as possible.
+               sql = re.sub(r"\n *", " ", sql)
+               sql = re.sub(r" += +", " = ", sql)
+               params = {"SQL": sql, "KeyField": biz.KeyField, "_method": 
"GET"}
+               prm = urllib.urlencode(params)
+               res = self.UrlOpener.open(url, data=prm)
+               encdata = res.read()
+               self._storeEncodedDataSet(encdata)
+       
+       
+       def save(self, startTransaction=False, allRows=False):
+               biz = self.obj
+               url = self._getFullUrl().replace(self.fname, "save")
+               changes = biz.getDataDiff(allRows=allRows)
+               chgDict = {hash(biz): (biz.DataSource, biz.KeyField, changes)}
+               params = {"DataDiff": dabo.lib.jsonEncode(chgDict), "_method": 
"POST"}
+               prm = urllib.urlencode(params)
+               try:
+                       res = self.UrlOpener.open(url, data=prm)
+               except urllib2.HTTPError, e:
+                       # There was a problem on the server side. Re-raise the 
appropriate
+                       # exception so that the UI can handle it.
+                       errcode = e.code
+                       errText = e.read()
+                       errMsg = "\n".join(errText.splitlines()[4:])
+                       if errcode == 409:
+                               raise dException.BusinessRuleViolation, errMsg
+                       elif errcode == 500:
+                               raise dException.ConnectionLostException, errMsg
+                       elif errcode == 204:
+                               raise dException.NoRecordsException, errMsg
+                       elif errcode == 400:
+                               raise dException.DBQueryException, errMsg
+               else:
+                       # If successful, we need to clear the mementos. We 
don't need to 
+                       # store anything; passing None will just  clear the 
mementos.
+                       self.obj._storeData(None, None)
+               
+
+       def saveAll(self, startTransaction=True):
+               self.save(startTransaction=startTransaction, allRows=True)
+
+
+       def delete(self):
+               biz = self.obj
+               url = self._getFullUrl()
+               params = {"PK": biz.getPK(), "KeyField": biz.KeyField, 
"_method": "DELETE"}
+               prm = urllib.urlencode(params)
+               res = self.UrlOpener.open(url, data=prm)
+               encdata = res.read()
+               self._storeEncodedDataSet(encdata)
+
+
+       def syncFiles(self):
+               app = self.obj
+               homedir = app.HomeDirectory
+               try:
+                       appname = file(os.path.join(homedir, ".appname")).read()
+               except IOError:
+                       # Use the HomeDirectory name
+                       appname = os.path.split(homedir.rstrip("/"))[1]
+               url = self._getManifestUrl(appname, "diff")
+               # Get the current manifest
+               currentMf = Manifest.getManifest(homedir)
+               params = {"current": dabo.lib.jsonEncode(currentMf)}
+               prm = urllib.urlencode(params)
+               try:
+                       res = self.UrlOpener.open(url, data=prm)
+               except urllib2.HTTPError, e:
+                       errcode = e.code
+                       errText = e.read()
+                       errMsg = "\n".join(errText.splitlines()[4:])
+                       if errcode == 304:
+                               # Nothing has changed on the server, so we're 
cool...
+                               return
+                       else:
+                               dabo.errorLog.write(_("HTTP Error syncing 
files: %s") % e)
+                               return
+               pickleRet = res.read()
+               filecode, chgs, serverMf = 
pickle.loads(dabo.lib.jsonDecode(pickleRet))
+               # Everything after this is relative to the app's home 
directory, so 
+               # change to it
+               currdir = os.getcwd()
+               os.chdir(homedir)
+               # Read in the current base manifest
+               try:
+                       baseMf = pickle.load(file(".serverManifest"))
+               except IOError:
+                       baseMf = {}
+               # Save the current server manifest
+               pickle.dump(serverMf, file(".serverManifest", "w"), 
pickle.HIGHEST_PROTOCOL)
+               # Check the server manifest for deleted files
+               deleted = [pth for (pth, modf) in chgs.items()
+                               if not modf]
+               for delpth in deleted:
+                       if delpth in baseMf:
+                               # Only delete files if they originally came 
from the server
+                               os.remove(delpth)
+               # A zero filecode represents no changed files
+               if filecode:
+                       # Request the files
+                       url = self._getManifestUrl("files", str(filecode))
+                       try:
+                               res = self.UrlOpener.open(url)
+                       except urllib2.HTTPError, e:
+                               dabo.errorLog.write(_("HTTP Error retrieving 
files: %s") % e)
+                       # res holds a zip file
+                       f = StringIO(res.read())
+                       zip = ZipFile(f)
+                       for pth in zip.namelist():
+                               tm = chgs.get(pth)
+                               dirname = os.path.split(pth)[0]
+                               if dirname and not os.path.exists(dirname):
+                                       os.makedirs(dirname)
+                               file(pth, "wb").write(zip.read(pth))
+                               os.utime(pth, (tm, tm))
+# NOT WORKING
+# Need to find a way to handle re-importing .py files.
+#                              if pth.endswith(".py"):
+#                                      # if this is a .py file, we want to try 
re-importing it
+#                                      modulePath = 
os.path.split(pth)[0].replace(os.sep, ".")
+#                                      thismod = 
os.path.splitext(pth)[0].replace(os.sep, ".")
+#                                      if thismod:
+#                                              print "THISMOD", thismod
+#                                              sys.modules.pop(thismod, None)
+#                                              __import__(thismod)
+#                                      if modulePath:
+#                                              modl = 
sys.modules.get(modulePath)
+#                                              print "pth", modulePath, 
type(modulePath), "MOD", modl
+#                                              if modl:
+#                                                      reload(modl)
+               os.chdir(currdir)
+
+
+       # These are not handled by the local bizobjs, so just skip these
+       def beginTransaction(self): pass
+       def commitTransaction(self): pass
+       def rollbackTransaction(self): pass
+
+
+       def _getConnection(self):
+               return self.obj._getConnection()
+
+
+       def _getPassword(self):
+               try:
+                       ci = self.Connection.ConnectInfo
+                       return ci.decrypt(ci.Password)
+               except AttributeError:
+                       return ""
+
+
+       def _getRemoteHost(self):
+               try:
+                       return urlparse.urlparse(self.UrlBase)[1]
+               except IndexError:
+                       return ""
+
+
+       def _getUrlBase(self):
+               try:
+                       ret = self.Connection.ConnectInfo.RemoteHost
+               except AttributeError:
+                       # Might be an application object
+                       try:
+                               ret = self.obj.SourceURL
+                       except AttributeError:
+                               ret = ""
+               else:
+                       app = dabo.dAppRef
+                       if app and not app.SourceURL:
+                               app.SourceURL = ret
+               return ret
+
+
+       def _getUrlOpener(self):
+               if self._urlOpener is None:
+                       # Create an OpenerDirector with support for HTTP Digest 
Authentication...
+                       auth_handler = urllib2.HTTPDigestAuthHandler()
+                       auth_handler.add_password(None, self.RemoteHost, 
self.UserName, self.Password)
+                       self._urlOpener = urllib2.build_opener(auth_handler)
+               return self._urlOpener
+
+
+       def _getUserName(self):
+               try:
+                       return self.Connection.ConnectInfo.User
+               except AttributeError:
+                       return ""
+
+
+       Connection = property(_getConnection, None, None,
+                       _("Reference to the connection object for the bizobj 
being decorated  (dabo.db.dConnection)"))
+
+       Password = property(_getPassword, None, None,
+                       _("Plain-text password for authentication on the remote 
server. (str)"))
+
+       RemoteHost = property(_getRemoteHost, None, None,
+                       _("Host to use as the remote server  (read-only) 
(str)"))
+
+       UrlBase = property(_getUrlBase, None, None,
+                       _("URL for the remote server  (read-only) (str)"))
+
+       UrlOpener = property(_getUrlOpener, None, None,
+                       _("Reference to the object that opens URLs and 
optionally authenticates.  (read-only) (urllib2.urlopener)"))
+
+       UserName = property(_getUserName, None, None,
+                       _("Username for authentication on the remote server  
(str)"))

Modified: trunk/dabo/lib/__init__.py
===================================================================
--- trunk/dabo/lib/__init__.py  2008-10-04 15:21:32 UTC (rev 4546)
+++ trunk/dabo/lib/__init__.py  2008-10-04 15:55:23 UTC (rev 4547)
@@ -9,7 +9,21 @@
 
 import uuid
 
+try:
+       import dejavuJSON
+except:
+       jsonConverter = None
+       def jsonEncode(val): raise ImportError, "The simplejson module is not 
installed"
+       def jsonDecode(val): raise ImportError, "The simplejson module is not 
installed"
+else:
+       jsonConverter = dejavuJSON.Converter()
+       def jsonEncode(val):
+               return jsonConverter.dumps(val)
+       
+       def jsonDecode(val):
+               return jsonConverter.loads(val)
 
+
 def getRandomUUID():
        return str(uuid.uuid4())
 

Modified: trunk/dabo/lib/connParser.py
===================================================================
--- trunk/dabo/lib/connParser.py        2008-10-04 15:21:32 UTC (rev 4546)
+++ trunk/dabo/lib/connParser.py        2008-10-04 15:55:23 UTC (rev 4547)
@@ -15,6 +15,7 @@
                                "name": "",
                                "dbtype" : "",
                                "host" : "",
+                               "remotehost" : "",
                                "database" : "",
                                "user" : "",
                                "password" : "",

Added: trunk/dabo/lib/dejavuJSON.py
===================================================================
--- trunk/dabo/lib/dejavuJSON.py                                (rev 0)
+++ trunk/dabo/lib/dejavuJSON.py        2008-10-04 15:55:23 UTC (rev 4547)
@@ -0,0 +1,149 @@
+"""JSON conversion support for Dejavu Units.
+
+The source for this was obtained from: http://www.aminus.net/dejavu
+
+It was published there with the statement that it was being placed in 
+the public domain. I have only modified it minimally for its inclusion 
+into Dabo.
+"""
+
+import datetime
+import decimal
+import time
+import sys
+
+try:
+       from simplejson import JSONEncoder, JSONDecoder
+except ImportError:
+       print """
+
+The required 'simplejson' module is not present. 
+
+Please install that module before using the web features of Dabo.
+
+"""
+       sys.exit()
+
+__all__ = ["Encoder", "Decoder", "Converter"]
+
+class Null(object):
+       class meta(type):
+               def __new__(cls, *args, **kwargs):
+                       if '_inst' not in vars(cls):
+                               cls._inst = type.__new__(cls, *args, **kwargs)
+                       return cls._inst
+       __metaclass__ = meta
+       def __init__(self, *args, **kwargs): pass
+       def __call__(self, *args, **kwargs): return self
+       def __repr__(self): return "Null()"
+       def __nonzero__(self): return False
+       def __getattr__(self, name): return self
+       def __setattr__(self, name, value): return self
+       def __delattr__(self, name): return self
+
+class Encoder(JSONEncoder):
+       """Extends the base simplejson JSONEncoder for Dejavu."""
+       
+       DATE_FMT = "%Y-%m-%d"
+       TIME_FMT = "%H:%M:%S"
+       DATETIME_FMT = "%s %s" % (DATE_FMT, TIME_FMT)
+       
+       def default(self, o):
+               _special_types = (datetime.date, datetime.time, 
datetime.datetime,
+                                                 decimal.Decimal)
+               # We MUST check for a datetime.datetime instance before 
datetime.date.
+               # datetime.datetime is a subclass of datetime.date, and 
therefore
+               # instances of it are also instances of datetime.date.
+               if isinstance(o, datetime.datetime) or o is datetime.datetime:
+                       if o is datetime.datetime:
+                               o = Null()
+                       return {'__datetime__': True,
+                                       'value': o.strftime(self.DATETIME_FMT)}
+               
+               if isinstance(o, datetime.date) or o is datetime.date:
+                       if o is datetime.date:
+                               o = Null()
+                       return {'__date__': True,
+                                       'value': o.strftime(self.DATE_FMT)}
+               
+               if isinstance(o, datetime.time) or o is datetime.time:
+                       if o is datetime.time:
+                               o = Null()
+                       return {'__time__': True,
+                                       'value': o.strftime(self.TIME_FMT)}
+               
+               if isinstance(o, decimal.Decimal) or o is decimal.Decimal:
+                       if o is decimal.Decimal:
+                               value = None
+                       else:
+                               value = unicode(o)
+                       return {'__decimal__': True,
+                                       'value': value}
+               
+               if isinstance(o, Null):
+                       return None
+               
+               else:
+                       return JSONEncoder.default(self, o)
+
+class Decoder(JSONDecoder):
+       """Extends the base simplejson JSONDecoder for Dejavu."""
+       
+       DATE_FMT = "%Y-%m-%d"
+       TIME_FMT = "%H:%M:%S"
+       DATETIME_FMT = "%s %s" % (DATE_FMT, TIME_FMT)
+       
+       def __init__(self, arena=None, encoding=None, object_hook=None):
+               JSONDecoder.__init__(self, encoding, object_hook)
+               if not self.object_hook:
+                       self.object_hook = self.json_to_python
+               self.arena = arena
+       
+       def json_to_python(self, d):
+               if '__datetime__' in d:
+                       if d['value'] is None:
+                               return None
+                       strp = time.strptime(d['value'], self.DATETIME_FMT)[:7]
+                       return datetime.datetime(*strp)
+               if '__date__' in d:
+                       if d['value'] is None:
+                               return None
+                       strp = time.strptime(d['value'], self.DATE_FMT)[:3]
+                       return datetime.date(*strp)
+               if '__time__' in d:
+                       if d['value'] is None:
+                               return None
+                       strp = time.strptime(d['value'], self.TIME_FMT)[3:6]
+                       return datetime.time(*strp)
+               if '__decimal__' in d:
+                       if d['value'] is None:
+                               return None
+                       return decimal.Decimal(d['value'])
+               return d
+
+class Converter(object):
+       """Provides two-way conversion of Units/JSON via loads and dumps 
methods.
+       
+       Also converts datetime.date, datetime.time, datetime.datetime and
+       decimal.Decimal to/from JSON.
+
+       This is accomplished by the Encoder and Decoder classes, which are
+       subclasses of their counterparts in simplejson.  If you wish to change
+       the output of the converter at all, you should probably subclass the
+       Encoder/Decoder and then make a cusom Converter subclass with your
+       encoder/decoder as class attributes.
+       """
+       
+       encoder = Encoder
+       decoder = Decoder
+       
+       def __init__(self, arena=None):
+               self.arena = arena
+       
+       def loads(self, s, encoding=None, **kw):
+               return self.decoder(encoding=encoding, arena=self.arena, 
**kw).decode(s)
+
+       def dumps(self, obj, skipkeys=False, ensure_ascii=False,
+                         check_circular=True, allow_nan=True, indent=None, 
**kw):
+               return self.encoder(skipkeys, ensure_ascii, check_circular,
+                                                       allow_nan, indent, 
**kw).encode(obj)
\ No newline at end of file

Added: trunk/dabo/lib/manifest.py
===================================================================
--- trunk/dabo/lib/manifest.py                          (rev 0)
+++ trunk/dabo/lib/manifest.py  2008-10-04 15:55:23 UTC (rev 4547)
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import sys
+import os
+import datetime
+import dabo
+
+
+class Manifest(object):
+       """This class encapsulates all of the methods needed to create and 
manage
+       a manifest system for syncing directories. 
+       
+       A manifest is simply a dictionary with the keys being the file paths, 
and the
+       values being a timestamp. Two manifests, referred to as 'source' and 
'target',
+       can be compared to find the changes required to make 'target' match 
'source'.
+       """
+       # These are the file types that are included by default.
+       includedTypes = ["py", "txt", "cnxml", "rfxml", "cdxml", "mnxml", "xml",
+                       "jpg" , "jpeg" , "gif" , "tif" , "tiff" , "png" , "ico" 
, "bmp" , "sh", "mo",

 (3340 bytes were truncated as it was too long for the email (max 40000 bytes.)


_______________________________________________
Post Messages to: [email protected]
Subscription Maintenance: http://leafe.com/mailman/listinfo/dabo-dev
Searchable Archives: http://leafe.com/archives/search/dabo-dev
This message: http://leafe.com/archives/byMID/[EMAIL PROTECTED]

Reply via email to