Changeset 2653

Show
Ignore:
Timestamp:
01/11/07 10:43:31 (2 years ago)
Author:
paul
Message:

This is a major rework of the mementos system in dCursorMixin. Previously, we
copied a memento for every record at the time of requery, and when it came to
check to see if there were any changes to any records, we crawled through each
parent record, then through children, their children, etc. etc. testing
each memento against each field. Now, we only record a memento when the field
value changes.

This commit also includes unit tests for dCursorMixin and dBizobj that test the
memento system.

Added scanRows() and scanChangedRows() functions to dBizobj, which do the same
as scan() but only for the applicable rows.

Added an _index() function to dDataSet, that returns the index (row number) of
the passed record object.

Reworked how dBizobj/dCursorMixin.DataStructure? gets the field type information.
Specifically, reworked dBackend.getStructureDescription() to first look at the
cursor.description, and then use getFields() to fill in any missing information.

Removed a duplicate conversion to dDataSet in dCursorMixin. Tried to find and
fix instances where we raise dException.dException and we should be raising
more explicit exceptions such as dException.FieldNotFoundException?.

Please note that I tested against MySQL and sqlite, not against Firebird,
PostgreSQL, or MS-SQL.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/dabo/biz/dBizobj.py

    r2643 r2653  
    254254 
    255255    def saveAll(self, startTransaction=False, topLevel=True): 
    256         """ Iterates through all the records of the bizobj, and calls save() 
    257         for any record that has pending changes. 
    258         """ 
     256        """Saves all changes to the bizobj and children.""" 
     257 
     258        useTransact = startTransaction or topLevel 
     259        old_pk = getattr(self.Record, self.KeyField, None) 
    259260        cursor = self._CurrentCursor 
     261 
     262        if useTransact: 
     263            # Tell the cursor to begin a transaction, if needed. 
     264            cursor.beginTransaction() 
     265         
     266        changed_rows = self.getChangedRows() 
     267        for row in changed_rows: 
     268            self._moveToRowNum(row) 
     269            try: 
     270                self.save(startTransaction=False, topLevel=False) 
     271            except dException.ConnectionLostException, e: 
     272                self._moveToPK(old_pk) 
     273                raise dException.ConnectionLostException, e 
     274            except dException.DBQueryException, e: 
     275                # Something failed; reset things. 
     276                if useTransact: 
     277                    cursor.rollbackTransaction() 
     278                # Pass the exception to the UI 
     279                self._moveToPK(old_pk) 
     280                raise dException.DBQueryException, e 
     281            except dException.dException, e: 
     282                if useTransact: 
     283                    cursor.rollbackTransaction() 
     284                self._moveToPK(old_pk) 
     285                raise 
     286 
     287        if useTransact: 
     288            cursor.commitTransaction() 
     289 
     290        if old_pk is not None: 
     291            self._moveToPK(old_pk) 
     292 
     293 
     294    def save(self, startTransaction=False, topLevel=True): 
     295        """Save any changes that have been made in the current row. 
     296 
     297        If the save is successful, the saveAll() of all child bizobjs will be 
     298        called as well. 
     299        """ 
     300        cursor = self._CurrentCursor 
     301        errMsg = self.beforeSave() 
     302        if errMsg: 
     303            raise dException.dException, errMsg 
     304 
     305        if self.KeyField is None: 
     306            raise dException.dException, _("No key field defined for table: ") + self.DataSource 
     307 
     308        # Validate any changes to the data. If there is data that fails 
     309        # validation, an Exception will be raised. 
     310        self._validate() 
     311 
    260312        useTransact = startTransaction or topLevel 
    261313        if useTransact: 
     
    263315            cursor.beginTransaction() 
    264316 
    265         try: 
    266             self.scan(self._saveRowIfChanged, startTransaction=False, topLevel=False) 
     317        # Save to the Database, but first save the IsAdding flag as the save() call 
     318        # will reset it to False: 
     319        isAdding = self.IsAdding 
     320        try: 
     321            cursor.save() 
     322            if isAdding: 
     323                # Call the hook method for saving new records. 
     324                self._onSaveNew() 
     325 
     326            # Iterate through the child bizobjs, telling them to save themselves. 
     327            for child in self.__children: 
     328                # No need to start another transaction. And since this is a child bizobj, 
     329                # we need to save all rows that have changed. 
     330                child.saveAll(startTransaction=False, topLevel=False) 
     331 
     332            # Finish the transaction, and requery the children if needed. 
     333            if useTransact: 
     334                cursor.commitTransaction() 
     335            if self.RequeryChildOnSave: 
     336                self.requeryAllChildren() 
     337 
    267338        except dException.ConnectionLostException, e: 
    268339            raise dException.ConnectionLostException, e 
     340 
     341        except dException.NoRecordsException, e: 
     342            # Nothing to roll back; just throw it back for the form to display 
     343            raise dException.NoRecordsException, e 
     344 
    269345        except dException.DBQueryException, e: 
    270346            # Something failed; reset things. 
     
    273349            # Pass the exception to the UI 
    274350            raise dException.DBQueryException, e 
    275         except dException.dException, e: 
    276             if useTransact: 
    277                 cursor.rollbackTransaction() 
    278             raise dException.dException, e 
    279  
    280         if useTransact: 
    281             cursor.commitTransaction() 
    282  
    283  
    284     def _saveRowIfChanged(self, startTransaction, topLevel): 
    285         """ Meant to be called as part of a scan loop. That means that we can 
    286         assume that the current record is the one we want to act on. Also, we 
    287         can pass False for the two parameters, since they will have already been 
    288         accounted for in the calling method. 
    289         """ 
    290         if self.isChanged(): 
    291             self.save(startTransaction, topLevel) 
    292  
    293  
    294     def save(self, startTransaction=False, topLevel=True): 
    295         """ Save any changes that have been made in the data set. If the 
    296         save is successful, the save() of all child bizobjs will be 
    297         called as well. 
    298         """ 
    299         cursor = self._CurrentCursor 
    300         errMsg = self.beforeSave() 
    301         if errMsg: 
    302             raise dException.dException, errMsg 
    303  
    304         if self.KeyField is None: 
    305             raise dException.dException, _("No key field defined for table: ") + self.DataSource 
    306  
    307         # Validate any changes to the data. If there is data that fails 
    308         # validation, an Exception will be raised. 
    309         self._validate() 
    310  
    311         useTransact = startTransaction or topLevel 
    312         if useTransact: 
    313             # Tell the cursor to begin a transaction, if needed. 
    314             cursor.beginTransaction() 
    315  
    316         # Save to the Database, but first save the IsAdding flag as the save() call 
    317         # will reset it to False: 
    318         isAdding = self.IsAdding 
    319         try: 
    320             cursor.save() 
    321             if isAdding: 
    322                 # Call the hook method for saving new records. 
    323                 self._onSaveNew() 
    324  
    325             # Iterate through the child bizobjs, telling them to save themselves. 
    326             for child in self.__children: 
    327                 # No need to start another transaction. And since this is a child bizobj, 
    328                 # we need to save all rows that have changed. 
    329                 child.saveAll(startTransaction=False, topLevel=False) 
    330  
    331             # Finish the transaction, and requery the children if needed. 
    332             if useTransact: 
    333                 cursor.commitTransaction() 
    334             if self.RequeryChildOnSave: 
    335                 self.requeryAllChildren() 
    336  
    337             self.setMemento() 
    338  
    339         except dException.ConnectionLostException, e: 
    340             raise dException.ConnectionLostException, e 
    341  
    342         except dException.NoRecordsException, e: 
    343             # Nothing to roll back; just throw it back for the form to display 
    344             raise dException.NoRecordsException, e 
    345  
    346         except dException.DBQueryException, e: 
    347             # Something failed; reset things. 
    348             if useTransact: 
    349                 cursor.rollbackTransaction() 
    350             # Pass the exception to the UI 
    351             raise dException.DBQueryException, e 
    352351 
    353352        except dException.dException, e: 
     
    356355                cursor.rollbackTransaction() 
    357356            # Pass the exception to the UI 
    358             raise dException.dException, e 
     357            raise 
    359358 
    360359        # Some backends (Firebird particularly) need to be told to write 
     
    369368 
    370369    def cancelAll(self): 
    371         """ Iterates through all the records, canceling each in turn. """ 
    372         self.scan(self.cancel
     370        """Cancel all changes made to the current dataset, including all children.""" 
     371        self.scanChangedRows(self.cancel, allCursors=True
    373372 
    374373 
    375374    def cancel(self): 
    376         """ Cancel any changes to the current record, reverting the fields 
    377         back to their original values. 
     375        """Cancel all changes to the current record and all children. 
     376 
     377        Two hook methods will be called: beforeCancel() and afterCancel(). The 
     378        former, if it returns an error message, will raise an exception and not 
     379        continue cancelling the record. 
    378380        """ 
    379381        errMsg = self.beforeCancel() 
    380         if not errMsg: 
    381             errMsg = self.beforePointerMove() 
    382382        if errMsg: 
    383383            raise dException.dException, errMsg 
    384384 
    385         # Tell the cursor to cancel any changes 
     385        # Tell the cursor and all children to cancel themselves: 
    386386        self._CurrentCursor.cancel() 
    387         # Tell each child to cancel themselves 
    388387        for child in self.__children: 
    389388            child.cancelAll() 
    390             child.requery() 
    391  
    392         self.setMemento() 
     389 
    393390        self.afterCancel() 
    394391 
    395392 
    396393    def delete(self, startTransaction=False): 
    397         """ Delete the current row of the data set.""" 
     394        """Delete the current row of the data set.""" 
    398395        cursor = self._CurrentCursor 
    399396        errMsg = self.beforeDelete() 
     
    471468 
    472469 
    473     def getChangedRecordNumbers(self): 
    474         """ Returns a list of record numbers for which isChanged() 
    475         returns True. The changes may therefore not be in the record 
    476         itself, but in a dependent child record. 
    477         """ 
    478         self.__changedRecordNumbers = [] 
    479         self.scan(self._listChangedRecordNumbers) 
    480         return self.__changedRecordNumbers 
    481  
    482  
    483     def _listChangedRecordNumbers(self): 
     470    def getChangedRows(self): 
     471        """ Returns a list of row numbers for which isChanged() returns True. The  
     472        changes may therefore not be in the record itself, but in a dependent child  
     473        record. 
     474        """ 
     475        if self.__children: 
     476            # Must iterate all records to find potential changes in children: 
     477            self.__changedRows = [] 
     478            self.scan(self._listChangedRows) 
     479            return self.__changedRows 
     480        else: 
     481            # Can use the much faster cursor.getChangedRows(): 
     482            return self._CurrentCursor.getChangedRows() 
     483 
     484 
     485    def _listChangedRows(self): 
    484486        """ Called from a scan loop. If the current record is changed, 
    485487        append the RowNumber to the list. 
    486488        """ 
    487489        if self.isChanged(): 
    488             self.__changedRecordNumbers.append(self.RowNumber) 
     490            self.__changedRows.append(self.RowNumber) 
    489491 
    490492 
     
    510512        self.__scanReverse is true, the records are processed in reverse order. 
    511513        """ 
    512         if self.RowCount <= 0: 
    513             # Nothing to scan! 
    514             return 
    515  
     514        self.scanRows(func, range(self.RowCount), *args, **kwargs) 
     515 
     516 
     517    def scanRows(self, func, rows, *args, **kwargs): 
     518        """Iterate over the specified rows and apply the passed function to each. 
     519 
     520        Set self.exitScan to True to exit the scan on the next iteration. 
     521        """ 
    516522        # Flag that the function can set to prematurely exit the scan 
    517523        self.exitScan = False 
     524        rows = list(rows) 
    518525        if self.__scanRestorePosition: 
    519526            currRow = self.RowNumber 
    520527        try: 
    521528            if self.__scanReverse: 
    522                 recRange = range(self.RowCount-1, -1, -1) 
    523             else: 
    524                 recRange = range(self.RowCount) 
    525             for i in recRange: 
     529                rows.reverse() 
     530            for i in rows: 
    526531                self._moveToRowNum(i) 
    527532                func(*args, **kwargs) 
     
    544549 
    545550 
     551    def scanChangedRows(self, func, allCursors=False, *args, **kwargs): 
     552        """Move the record pointer to each changed row, and call func. 
     553 
     554        If allCursors is True, all other cursors for different parent records will  
     555        be iterated as well.  
     556 
     557        If you want to end the scan on the next iteration, set self.exitScan=True. 
     558 
     559        Records are scanned in arbitrary order. Any exception raised by calling 
     560        func() will be passed   up to the caller. 
     561        """ 
     562        self.exitScan = False 
     563        old_currentCursorKey = self.__currentCursorKey 
     564        old_pk = getattr(self.Record, self.KeyField, None) 
     565 
     566        if allCursors: 
     567            cursors = self.__cursors 
     568        else: 
     569            cursors = [{None: self._CurrentCursor}] 
     570 
     571        for key, cursor in cursors.iteritems(): 
     572            self._CurrentCursor = key 
     573            changed_keys = list(set(cursor._mementos.keys() + cursor._newRecords.keys())) 
     574            for pk in changed_keys: 
     575                self._moveToPK(pk) 
     576                try: 
     577                    func(*args, **kwargs) 
     578                except: 
     579                    # Reset things and bail: 
     580                    self._CurrentCursor = old_currentCursorKey 
     581                    self._moveToPK(old_pk) 
     582                    raise 
     583         
     584        self._CurrentCursor = old_currentCursorKey 
     585        if old_pk is not None: 
     586            self._moveToPK(old_pk) 
     587 
     588 
    546589    def getFieldNames(self): 
    547590        """Returns a tuple of all the field names in the cursor.""" 
     
    574617 
    575618        self._CurrentCursor.new() 
    576         # Hook method for things to do after a new record is created. 
    577619        self._onNew() 
    578620 
     
    586628                    child.new() 
    587629 
    588         self.setMemento() 
    589630        self.afterPointerMove() 
    590631        self.afterNew() 
     
    662703        except dException.NoRecordsException: 
    663704            pass 
    664         self.setMemento() 
    665705        self.afterRequery() 
    666706 
     
    832872 
    833873 
    834     def isAnyChanged(self): 
    835         """ Returns True if any record in the current record set has been 
    836         changed. 
    837         """ 
    838         self.__areThereAnyChanges = False 
    839         self.scan(self._checkForChanges) 
    840         return self.__areThereAnyChanges 
    841  
    842  
    843     def _checkForChanges(self): 
    844         """ Designed to be called from the scan iteration over the records 
    845         for this bizobj. Once one changed record is found, set the scan's 
    846         exit flag, since we only need to know if anything has changed. 
    847         """ 
    848         if self.isChanged(): 
    849             self.__areThereAnyChanges = True 
    850             self.exitScan = True 
     874    def isAnyChanged(self, _topLevel=True): 
     875        """Returns True if any record in the current record set has been changed.""" 
     876 
     877        if _topLevel: 
     878            # Only check the _CurrentCursor: 
     879            if self._CurrentCursor.isChanged(allRows=True): 
     880                return True 
     881        else: 
     882            # Need to check all cached cursors: 
     883            for cursor in self.__cursors.values(): 
     884                if cursor.isChanged(allRows=True): 
     885                    return True 
     886     
     887        # Nothing's changed in the top level, so we need to recurse the children: 
     888        for child in self.__children: 
     889            if child.isAnyChanged(_topLevel=False): 
     890                return True 
     891         
     892        return False 
    851893 
    852894 
     
    864906            # No cursor, no changes. 
    865907            return False 
    866         ret = cc.isChanged(allRows = False) 
     908        ret = cc.isChanged(allRows=False) 
    867909 
    868910        if not ret: 
     
    903945        """ 
    904946        cursor = self._CurrentCursor 
    905         cursor.setDefaults(self.DefaultValues) 
    906947        if self.AutoPopulatePK: 
    907948            # Provide a temporary PK so that any linked children can be properly 
     
    911952        if self.Parent and self.FillLinkFromParent and self.LinkField: 
    912953            self.setParentFK() 
     954        cursor.setDefaults(self.DefaultValues) 
     955        cursor.setNewFlag() 
     956 
    913957        # Call the custom hook method 
    914958        self.onNew() 
     
    10501094            # Let the child know the current dependent PK 
    10511095            child.setCurrentParent(pk) 
    1052             if not child.isChanged() and child.RequeryWithParent: 
    1053                 child.requery() 
    1054  
     1096            if child.RequeryWithParent: 
     1097                if not child.isChanged(): 
     1098                    child.requery() 
     1099     
    10551100        self.afterChildRequery() 
    10561101 
     
    11401185        """ 
    11411186        return self.__params 
    1142  
    1143  
    1144     def setMemento(self): 
    1145         """ Take a snapshot of the data in the cursor. 
    1146  
    1147         Tell the cursor to take a snapshot of the current state of the 
    1148         data. This snapshot will be used to determine what, if anything, has 
    1149         changed later on. 
    1150  
    1151         User code should not normally call this method. 
    1152         """ 
    1153         self._CurrentCursor.setMemento() 
    11541187 
    11551188 
  • trunk/dabo/biz/test/test_dBizobj.py

    r2649 r2653  
    33from dabo.lib import getRandomUUID 
    44 
    5  
    6 # Testing anything other than sqlite requires network access. So set these 
    7 # flags so that only the db's you want to test against are True. 
    8 test_sqlite = True 
    9 test_mysql = True 
    10  
    11 if test_sqlite: 
    12     sqlite_unittest = unittest.TestCase 
    13 else: 
    14     sqlite_unittest = object 
    15  
    16 if test_mysql: 
    17     mysql_unittest = unittest.TestCase 
    18 else: 
    19     mysql_unittest = object 
    20  
    21  
    22 class Test_dBizobj(object): 
     5## Only tests against sqlite, as we already test dCursorMixin against the 
     6## various backends.  
     7 
     8class Test_dBizobj(unittest.TestCase): 
    239    def setUp(self): 
    24         biz = self.biz 
     10        self.con = dabo.db.dConnection(DbType="SQLite", Database=":memory:") 
     11        biz = self.biz = dabo.biz.dBizobj(self.con) 
    2512        uniqueName = getRandomUUID().replace("-", "")[-20:] 
    2613        self.temp_table_name = "unittest%s" % uniqueName 
     
    3623 
    3724    def createSchema(self): 
    38         """Create the test schema. Override in subclasses.""" 
    39         pass 
     25        biz = self.biz 
     26        tableName = self.temp_table_name 
     27        childTableName = self.temp_child_table_name 
     28        biz._CurrentCursor.executescript(""" 
     29create table %(tableName)s (pk INTEGER PRIMARY KEY AUTOINCREMENT, cField CHAR, iField INT, nField DECIMAL (8,2)); 
     30insert into %(tableName)s (cField, iField, nField) values ("Paul Keith McNett", 23, 23.23); 
     31insert into %(tableName)s (cField, iField, nField) values ("Edward Leafe", 42, 42.42); 
     32insert into %(tableName)s (cField, iField, nField) values ("Carl Karsten", 10223, 23032.76); 
     33 
     34create table %(childTableName)s (pk INTEGER PRIMARY KEY AUTOINCREMENT, parent_fk INT, cInvNum CHAR); 
     35insert into %(childTableName)s (parent_fk, cInvNum) values (1, "IN00023"); 
     36insert into %(childTableName)s (parent_fk, cInvNum) values (1, "IN00455"); 
     37insert into %(childTableName)s (parent_fk, cInvNum) values (3, "IN00024"); 
     38""" % locals()) 
    4039 
    4140 
     
    102101        biz.Record.cField = "Denise McNett" 
    103102        self.assertEqual(biz.Record.cField, "Denise McNett") 
     103        self.assertEqual(cur._mementos[biz.Record.pk]["cField"], "Paul Keith McNett") 
    104104        biz.Record.cField = "Alison Anton" 
    105105        self.assertEqual(biz.Record.cField, "Alison Anton") 
     106        self.assertEqual(cur._mementos[biz.Record.pk]["cField"], "Paul Keith McNett") 
    106107        biz.setFieldVal("iField", 80) 
    107108        self.assertEqual(biz.Record.iField, 80) 
    108109        self.assertTrue(isinstance(biz.Record.iField, (int, long))) 
     110        self.assertEqual(cur._mementos[self.biz.Record.pk]["iField"], 23) 
    109111 
    110112    def test_RowCount(self): 
     
    140142    ## - End property unit tests - 
    141143 
     144 
     145    def testMementos(self): 
     146        biz = self.biz 
     147        cur = biz._CurrentCursor 
     148 
     149        priorVal = biz.Record.cField 
     150 
     151        # Make a change that is the same as the prior value: 
     152        biz.Record.cField = priorVal 
     153        self.assertEqual(priorVal, biz.Record.cField) 
     154 
     155        # Make a change that is different: 
     156        biz.Record.cField = "New test value" 
     157        self.assertEqual(cur._mementos, {biz.Record.pk: {"cField": priorVal}}) 
     158        self.assertEqual(biz.isChanged(), True) 
     159 
     160        # Change it back: 
     161        biz.Record.cField = priorVal 
     162        self.assertEqual(cur._mementos, {}) 
     163        self.assertEqual(biz.isChanged(), False) 
     164 
     165        # Make a change that is different and cancel: 
     166        biz.Record.cField = "New test value" 
     167        biz.cancel() 
     168        self.assertEqual(cur._mementos, {}) 
     169        self.assertEqual(biz.isChanged(), False) 
     170 
     171        # Add a record: 
     172        biz.new() 
     173         
     174        self.assertEqual(biz.RowCount, 4) 
     175        self.assertEqual(biz.RowNumber, 3) 
     176        self.assertEqual(cur._newRecords, {"-1-dabotmp": None}) 
     177        self.assertEqual(biz.isChanged(), True) 
     178        self.assertEqual(cur.Record.pk, "-1-dabotmp") 
     179        self.assertEqual(biz.Record.cField, "") 
     180        self.assertEqual(biz.Record.iField, 0) 
     181        self.assertEqual(biz.Record.nField, 0) 
     182        biz.save() 
     183        biz.requery() 
     184        self.assertEqual(biz.RowCount, 4) 
     185        self.assertEqual(biz.RowNumber, 3) 
     186        self.assertEqual(cur._newRecords, {}) 
     187        self.assertEqual(biz.isChanged(), False) 
     188        self.assertEqual(biz.Record.pk, 4) 
     189 
     190        # The new fields should be NULL, since we didn't explicitly set them: 
     191        self.assertEqual(biz.Record.cField, None) 
     192        self.assertEqual(biz.Record.iField, None) 
     193        self.assertEqual(biz.Record.nField, None) 
     194 
    142195    def testChildren(self): 
    143196        bizMain = self.biz 
     
    147200        bizChild.DataSource = self.temp_child_table_name 
    148201        bizChild.LinkField = "parent_fk" 
     202        bizChild.FillLinkFromParent = True 
    149203         
    150204        bizMain.addChild(bizChild) 
     
    224278 
    225279        bizMain.saveAll() 
     280 
     281        self.assertEqual(bizMain.RowCount, 3) 
     282        self.assertEqual(bizMain.RowNumber, 2) 
     283        self.assertEqual(bizChild.RowCount, 1) 
     284        self.assertEqual(bizChild.Record.cInvNum, "IN99991") 
     285        self.assertEqual(bizChild.IsAdding, False) 
    226286        bizMain.requery() 
    227287        self.assertEqual(bizMain.RowCount, 3) 
     
    233293        self.assertEqual(bizChild.RowCount, 1) 
    234294        self.assertEqual(bizChild.Record.cInvNum, "IN99991") 
    235  
    236  
    237 class Test_dBizobj_sqlite(Test_dBizobj, sqlite_unittest): 
    238     def setUp(self): 
    239         self.con = dabo.db.dConnection(DbType="SQLite", Database=":memory:") 
    240         self.biz = dabo.biz.dBizobj(self.con) 
    241         super(Test_dBizobj_sqlite, self).setUp() 
    242  
    243     def createSchema(self): 
    244         biz = self.biz 
    245         tableName = self.temp_table_name 
    246         childTableName = self.temp_child_table_name 
    247         biz._CurrentCursor.executescript(""" 
    248 create table %(tableName)s (pk INTEGER PRIMARY KEY AUTOINCREMENT, cField CHAR, iField INT, nField DECIMAL (8,2)); 
    249 insert into %(tableName)s (cField, iField, nField) values ("Paul Keith McNett", 23, 23.23); 
    250 insert into %(tableName)s (cField, iField, nField) values ("Edward Leafe", 42, 42.42); 
    251 insert into %(tableName)s (cField, iField, nField) values ("Carl Karsten", 10223, 23032.76); 
    252  
    253 create table %(childTableName)s (pk INTEGER PRIMARY KEY AUTOINCREMENT, parent_fk INT, cInvNum CHAR); 
    254 insert into %(childTableName)s (parent_fk, cInvNum) values (1, "IN00023"); 
    255 insert into %(childTableName)s (parent_fk, cInvNum) values (1, "IN00455"); 
    256 insert into %(childTableName)s (parent_fk, cInvNum) values (3, "IN00024"); 
    257 """ % locals()) 
    258  
    259  
    260 class Test_dBizobj_mysql(Test_dBizobj, mysql_unittest): 
    261     def setUp(self): 
    262         self.con = dabo.db.dConnection(DbType="MySQL", User="dabo_unittest",  
    263                 password="T30T35DB4K30Z45I67N60", Database="dabo_unittest", 
    264                 Host="paulmcnett.com") 
    265         self.biz = dabo.biz.dBizobj(self.con) 
    266         super(Test_dBizobj_mysql, self).setUp() 
    267  
    268     def tearDown(self): 
    269         self.biz._CurrentCursor.execute("drop table %s" % self.temp_table_name) 
    270         super(Test_dBizobj_mysql, self).tearDown() 
    271  
    272     def createSchema(self): 
    273         biz = self.biz 
    274         cur = biz._CurrentCursor 
    275         tableName = self.temp_table_name 
    276         childTableName = self.temp_child_table_name 
    277         cur.execute(""" 
    278 create table %s (pk INTEGER PRIMARY KEY AUTO_INCREMENT, cField CHAR (32), iField INT, nField DECIMAL (8,2)) 
    279 """ % tableName) 
    280         cur.execute("""      
    281 insert into %s (cField, iField, nField) values ("Paul Keith McNett", 23, 23.23) 
    282 """ % tableName) 
    283         cur.execute("""      
    284 insert into %s (cField, iField, nField) values ("Edward Leafe", 42, 42.42) 
    285 """ % tableName) 
    286         cur.execute("""      
    287 insert into %s (cField, iField, nField) values ("Carl Karsten", 10223, 23032.76) 
    288 """ % tableName) 
    289  
    290         cur.execute(""" 
    291 create table %s (pk INTEGER PRIMARY KEY AUTO_INCREMENT, parent_fk INT, cInvNum CHAR (16)) 
    292 """ % childTableName) 
    293         cur.execute(""" 
    294 insert into %s (parent_fk, cInvNum) values (1, "IN00023") 
    295 """ % childTableName) 
    296         cur.execute(""" 
    297 insert into %s (parent_fk, cInvNum) values (1, "IN00455") 
    298 """ % childTableName) 
    299         cur.execute(""" 
    300 insert into %s (parent_fk, cInvNum) values (3, "IN00024") 
    301 """ % childTableName) 
    302  
     295         
    303296 
    304297if __name__ == "__main__": 
  • trunk/dabo/db/__init__.py

    r2344 r2653  
    3434from dConnection import dConnection 
    3535from dCursorMixin import dCursorMixin 
    36 from dMemento import dMemento 
    3736from dConnectInfo import dConnectInfo 
    3837from dTable import dTable 
  • trunk/dabo/db/dBackend.py

    r2582 r2653  
    396396 
    397397    def getStructureDescription(self, cursor): 
    398         """This will work for most backends. However, SQLite doesn't 
    399         properly return the structure when no records are returned. 
    400         """ 
    401         #Try using the no-records version of the SQL statement. 
    402         try: 
    403             tmpsql = cursor.getStructureOnlySql() 
    404         except AttributeError: 
    405             # We need to parse the sql property to get what we need. 
    406             import re 
    407             pat = re.compile("(\s*select\s*.*\s*from\s*.*\s*)((?:where\s(.*))+)\s*", re.I | re.M | re.S) 
    408             if pat.search(cursor.sql): 
    409                 # There is a WHERE clause. Add the NODATA clause 
    410                 tmpsql = pat.sub("\\1 where 1=0 ", cursor.sql) 
    411             else: 
    412                 # no WHERE clause. See if it has GROUP BY or ORDER BY clauses 
    413                 pat = re.compile("(\s*select\s*.*\s*from\s*.*\s*)((?:group\s*by\s(.*))+)\s*", re.I | re.M | re.S) 
    414                 if pat.search(cursor.sql): 
    415                     tmpsql = pat.sub("\\1 where 1=0 ", cursor.sql) 
    416                 else: 
    417                     pat = re.compile("(\s*select\s*.*\s*from\s*.*\s*)((?:order\s*by\s(.*))+)\s*", re.I | re.M | re.S) 
    418                     if pat.search(cursor.sql): 
    419                         tmpsql = pat.sub("\\1 where 1=0 ", cursor.sql) 
    420                     else: 
    421                         # Nothing. So just tack it on the end. 
    422                         tmpsql = cursor.sql + " where 1=0 " 
    423         #print tmpsql 
    424         auxCrs = cursor._getAuxCursor() 
    425         auxCrs.execute(tmpsql) 
    426         auxCrs.storeFieldTypes() 
    427         return auxCrs.FieldDescription 
    428  
     398        """Return the basic field structure.""" 
     399        field_structure = {} 
     400        field_names = [] 
     401 
     402        field_description = cursor.FieldDescription 
     403        if not field_description: 
     404            # No query run yet: execute the structure-only sql: 
     405            structure_only_sql = cursor.getStructureOnlySql() 
     406            aux = cursor.AuxCursor 
     407            aux.execute(structure_only_sql) 
     408            field_description = aux.FieldDescription 
     409 
     410        for field_info in field_description: 
     411            field_name = field_info[0] 
     412            field_type = self.getDaboFieldType(field_info[1]) 
     413            field_names.append(field_name) 
     414            field_structure[field_name] = (field_type, False) 
     415 
     416        standard_fields = cursor.getFields() 
     417        for field_name, field_type, pk in standard_fields: 
     418            if field_name in field_names or not field_names: 
     419                # We only use the info for the standard field in one of two cases: 
     420                #   1) There aren't any fields in the FieldDescription, which would be 
     421                #      the case if we haven't set the SQL or requeried yet. 
     422                #   2) The field exists in the FieldDescription, and FieldDescription 
     423                #      didn't provide good type information. 
     424                if field_structure[field_name][0] == "?": 
     425                    # Only override what was in FieldStructure if getFields() gave better info. 
     426                    field_structure[field_name] = (field_type, pk) 
     427                if pk is True: 
     428                    # FieldStructure doesn't provide pk information: 
     429                    field_structure[field_name] = (field_structure[field_name][0], pk) 
     430 
     431        ret = [] 
     432        for field in field_names: 
     433            ret.append( (field, field_structure[field][0], field_structure[field][1]) ) 
     434        return tuple(ret) 
     435         
    429436 
    430437    ##########      Created by Echo     ############## 
  • trunk/dabo/db/dCursorMixin.py

    r2649 r2653  
    1717import dabo 
    1818import dabo.dConstants as kons 
    19 from dabo.db.dMemento import dMemento 
    2019from dabo.dLocalize import _ 
    2120import dabo.dException as dException 
     
    108107        self._cursorFactoryClass = None 
    109108 
     109        # mementos and new records, keyed on record object ids: 
     110        self._mementos = {} 
     111        self._newRecords = {} 
     112 
    110113        self.initProperties() 
    111114 
     
    168171                    # Fields of any type can be None (NULL). 
    169172                    pass 
     173                elif _USE_DECIMAL and type(field_val) in (float,) \ 
     174                        and pythonType in (Decimal,): 
     175                    ret = pythonType(str(field_val)) 
    170176                else: 
    171177                    try: 
     
    292298                                _fromRequery=_fromRequery) 
    293299 
    294             # Convert to dDataSet  
    295             self._records = dDataSet(self._records) 
    296300        return res 
    297301     
     
    313317        self.execute(self.CurrentSQL, params, _fromRequery=True) 
    314318         
    315         # Add mementos to each row of the result set 
    316         self.addMemento(-1) 
     319        # clear mementos and new record flags: 
     320        self._mementos = {} 
     321        self._newRecords = {} 
    317322 
    318323        # Check for any derived fields that should not be included in  
     
    558563        for rec in self._records: 
    559564            recInfo = [ colTemplate % (k, self.getType(v), self.escape(v))  
    560                     for k,v in rec.items()  
    561                     if k != "dabo-memento"] 
     565                    for k,v in rec.items() ] 
    562566            rowXML += rowTemplate % "\n".join(recInfo) 
    563567        return base % (self.Encoding, self.AutoPopulatePK, self.KeyField,  
     
    609613 
    610614    def isChanged(self, allRows=True): 
    611         """Scan all the records and compare them with their mementos.  
    612         Returns True if any differ, False otherwise. 
    613         """ 
    614         ret = False 
    615         if self.RowCount > 0: 
    616             if allRows: 
    617                 recs = self._records 
    618             else: 
    619                 recs = (self._records[self.RowNumber],) 
    620  
    621             for rec in recs: 
    622                 if self.isRowChanged(rec): 
    623                     ret = True 
    624                     break 
    625         return ret 
    626  
    627  
    628     def isRowChanged(self, rec): 
    629         ret = False 
    630         if rec.has_key(kons.CURSOR_MEMENTO): 
    631             mem = rec[kons.CURSOR_MEMENTO] 
    632             newrec = rec.has_key(kons.CURSOR_NEWFLAG) 
    633             ret = newrec or mem.isChanged(rec) 
    634         return ret 
    635  
    636  
    637     def setMemento(self): 
    638         if self.RowCount > 0: 
    639             if (self.RowNumber >= 0) and (self.RowNumber < self.RowCount): 
    640                 self.addMemento(self.RowNumber) 
    641      
    642      
     615        """Return True if there are any changes to the local field values. 
     616 
     617        If allRows is True (the default), all records in the recordset will be  
     618        considered. Otherwise, only the current record will be checked. 
     619        """ 
     620        if allRows: 
     621            return len(self._mementos) > 0 or len(self._newRecords) > 0 
     622        else: 
     623            row = self.RowNumber 
     624 
     625            try: 
     626                rec = self._records[row] 
     627            except IndexError: 
     628                # self.RowNumber doesn't exist (init phase?) Nothing's changed: 
     629                return False 
     630             
     631            memento = self._mementos.get(rec[self.KeyField], None) 
     632            new_rec = self._newRecords.has_key(rec[self.KeyField]) 
     633 
     634            return not (memento is None and not new_rec) 
     635 
     636 
     637    def setNewFlag(self): 
     638        """Set the current record to be flagged as a new record. 
     639 
     640        dBizobj will automatically call this method as appropriate, but if you are 
     641        using dCursor without a proxy dBizobj, you'll need to manually call this  
     642        method after cursor.new(), and (if applicable) after cursor.genTempAutoPK(). 
     643        For example: 
     644            cursor.new() 
     645            cursor.genTempAutoPK() 
     646            cursor.setNewFlag() 
     647        """  
     648        if self.KeyField: 
     649            rec = self._records[self.RowNumber] 
     650            self._newRecords[rec[self.KeyField]] = None 
     651 
     652 
    643653    def genTempAutoPK(self): 
    644654        """ Create a temporary PK for a new record. Set the key field to this 
     
    677687            raise dException.NoRecordsException, _("No records in the data set.") 
    678688        rec = self._records[self.RowNumber] 
    679         if rec.has_key(kons.CURSOR_NEWFLAG) and self.AutoPopulatePK: 
     689        if self._newRecords.has_key(rec[self.KeyField]) and self.AutoPopulatePK: 
    680690            # New, unsaved record 
    681691            ret = rec[kons.CURSOR_TMPKEY_FIELD] 
     
    733743         
    734744 
    735     def setFieldVal(self, fld, val): 
    736         """ Set the value of the specified field. """ 
     745    def _hasValidKeyField(self): 
     746        """Return True if the KeyField exists and names valid fields.""" 
     747        try: 
     748            self.checkPK() 
     749        except dException.MissingPKException: 
     750            return False 
     751        return True 
     752 
     753 
     754    def setFieldVal(self, fld, val, row=None): 
     755        """Set the value of the specified field.""" 
    737756        if self.RowCount <= 0: 
    738757            raise dException.NoRecordsException, _("No records in the data set") 
    739         else: 
    740             rec = self._records[self.RowNumber] 
    741             if rec.has_key(fld): 
    742                 if self._types.has_key(fld): 
    743                     fldType = self._types[fld] 
     758 
     759        if row is None: 
     760            row = self.RowNumber 
     761 
     762        rec = self._records[row] 
     763        valid_pk = self._hasValidKeyField() 
     764        keyField = self.KeyField 
     765 
     766        if not rec.has_key(fld): 
     767            ss = _("Field '%s' does not exist in the data set.") % (fld,) 
     768            raise dException.FieldNotFoundException, ss 
     769 
     770        if self._types.has_key(fld): 
     771            fldType = self._types[fld] 
     772        else: 
     773            fldType = self._fldTypeFromDB(fld) 
     774        if fldType is not None: 
     775            if fldType != type(val): 
     776                convTypes = (str, unicode, int, float, long, complex) 
     777                if isinstance(val, convTypes) and isinstance(rec[fld], basestring): 
     778                    if isinstance(fldType, str): 
     779                        val = str(val) 
     780                    else: 
     781                        val = unicode(val) 
     782                elif isinstance(rec[fld], int) and isinstance(val, bool): 
     783                    # convert bool to int (original field val was bool, but UI 
     784                    # changed to int.  
     785                    val = int(val) 
     786                elif isinstance(rec[fld], int) and isinstance(val, long): 
     787                    # convert long to int (original field val was int, but UI 
     788                    # changed to long.  
     789                    val = int(val) 
     790                elif isinstance(rec[fld], long) and isinstance(val, int): 
     791                    # convert int to long (original field val was long, but UI 
     792                    # changed to int.  
     793                    val = long(val) 
     794 
     795            if fldType != type(val): 
     796                ignore = False 
     797                # Date and DateTime types are handled as character, even if the  
     798                # native field type is not. Ignore these. NOTE: we have to deal with the  
     799                #