Changeset 2960

Show
Ignore:
Timestamp:
03/23/07 15:40:35 (1 year ago)
Author:
paul
Message:

Merged recent trunk changes into Carl's branch, and resolved (or attempted to)
some conflicts in dCursorMixin.

Carl, can you re-review all the pertinent areas? We are failing one of the
db/tests/test_dCursorMixin.py tests.

The main thing that seemed to change was that you were calling encloseSpaces()
but I guess that method went away and was replaced by encloseNames().

As soon as we have your branch working and up-to-date with trunk, we'll merge
the differences into trunk and the parameterization project will be complete.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • branches/carl2/INSTALL

    r2412 r2960  
    22 
    33PREREQUISITES: 
    4 + Python (we recommend the latest, currently 2.4.x) 
     4+ Python (we recommend the latest, currently 2.5) 
     5+ sqlite3 (comes with Python 2.5) or pysqlite2 (install separately) 
     6+ wxPython UNICODE BUILD (we recommend the latest, currently 2.8) 
    57+ MySQLdb (to run the demos) 
    6 + wxPython (we recommend the latest, currently 2.6.x) 
    78+ reportlab 
     9+ PIL (also known as Imaging or the Python Imaging Library) 
    810 
    911INSTALLATION: 
  • branches/carl2/dabo/__init__.py

    r2636 r2960  
    130130errorLog.Caption = "Dabo Error Log" 
    131131errorLog.LogObject = sys.stderr 
     132# This log is set to None by default. It must be manually activated  
     133# via the Application object. 
     134dbActivityLog = Log() 
     135dbActivityLog.Caption = "Database Activity Log" 
     136dbActivityLog.LogObject = None 
    132137 
    133138# Import global settings (do this first, as other imports may rely on it): 
  • branches/carl2/dabo/biz/dBizobj.py

    r2748 r2960  
    3232 
    3333        self._beforeInit() 
    34         self._conn = conn 
    35  
    36         super(dBizobj, self).__init__(properties=properties, *args, **kwargs) 
    37  
    38         cn = self._conn 
    39         if cn: 
     34        cf = self._cursorFactory = conn 
     35        if cf: 
    4036            # Base cursor class : the cursor class from the db api 
    41             self.dbapiCursorClass = cn.getDictCursorClass() 
     37            self.dbapiCursorClass = cf.getDictCursorClass() 
    4238 
    4339            # If there are any problems in the createCursor process, an 
     
    4541            self.createCursor() 
    4642 
    47  
    4843        # We need to make sure the cursor is created *before* the call to 
    4944        # initProperties() 
    5045        self._initProperties() 
     46        super(dBizobj, self).__init__(properties=properties, *args, **kwargs) 
    5147        self._afterInit() 
    5248        self.__att_try_setFieldVal = True 
     
    5652        # Cursor to manage SQL Builder info. 
    5753        self._sqlMgrCursor = None 
    58         self._conn = None 
     54        self._cursorFactory = None 
    5955        self.__params = ()      # tuple of params to be merged with the sql in the cursor 
    6056        self.__children = []        # Collection of child bizobjs 
     
    6763        self._linkField = "" 
    6864        self._parentLinkField = "" 
    69         # Used the the addChildByRelationDict() method to eliminate infinite loops 
     65        # Used the the _addChildByRelationDict() method to eliminate infinite loops 
    7066        self.__relationDictSet = False 
    7167        # Do we try to same on the same record during a requery? 
     
    7975        self._parent  = None 
    8076        self._autoPopulatePK = True 
     77        self._autoQuoteNames = True 
    8178        self._keyField = "" 
    8279        self._requeryChildOnSave = False 
     
    110107        of the cursor. 
    111108        """ 
    112         cn = self._conn 
     109        cf = self._cursorFactory 
    113110        cursorClass = self._getCursorClass(self.dCursorMixinClass, 
    114111                self.dbapiCursorClass) 
    115         crs = cn.getCursor(cursorClass) 
    116         crs.BackendObject = cn.getBackendObject() 
    117         crs.setCursorFactory(cn.getCursor, cursorClass) 
     112        crs = cf.getCursor(cursorClass) 
     113        crs.BackendObject = cf.getBackendObject() 
     114        crs.setCursorFactory(cf.getCursor, cursorClass) 
    118115        return crs 
    119116         
     
    131128        if self.__cursors: 
    132129            _dataStructure = getattr(self.__cursors[self.__cursors.keys()[0]], "_dataStructure", None) 
     130            _virtualFields = getattr(self.__cursors[self.__cursors.keys()[0]], "_virtualFields", {}) 
    133131        else: 
    134132            # The first cursor is being created, before DataStructure is assigned. 
    135133            _dataStructure = None 
     134            _virtualFields = {} 
    136135        errMsg = self.beforeCreateCursor() 
    137136        if errMsg: 
     
    144143            key = self.__currentCursorKey 
    145144 
    146         cn = self._conn 
    147         self.__cursors[key] = cn.getCursor(cursorClass) 
    148         self.__cursors[key].setCursorFactory(cn.getCursor, cursorClass) 
     145        cf = self._cursorFactory 
     146        self.__cursors[key] = cf.getCursor(cursorClass) 
     147        self.__cursors[key].setCursorFactory(cf.getCursor, cursorClass) 
    149148 
    150149        crs = self.__cursors[key] 
    151150        if _dataStructure is not None: 
    152151            crs._dataStructure = _dataStructure 
     152        crs._virtualFields = _virtualFields 
    153153        crs.KeyField = self.KeyField 
    154154        crs.Table = self.DataSource 
    155155        crs.AutoPopulatePK = self.AutoPopulatePK 
    156         crs.BackendObject = cn.getBackendObject() 
     156        crs.AutoQuoteNames = self.AutoQuoteNames 
     157        crs.BackendObject = cf.getBackendObject() 
    157158        crs.sqlManager = self.SqlManager 
    158159        crs.AutoCommit = self.AutoCommit 
    159160        crs._bizobj = self 
    160         crs.setSQL(self.SQL) 
    161161        if self.RequeryOnLoad: 
    162162            crs.requery() 
     
    270270                self.save(startTransaction=False, topLevel=False) 
    271271            except dException.ConnectionLostException, e: 
    272                 self._moveToPK(old_pk) 
     272                self.RowNumber = current_row 
    273273                raise dException.ConnectionLostException, e 
    274274            except dException.DBQueryException, e: 
     
    277277                    cursor.rollbackTransaction() 
    278278                # Pass the exception to the UI 
    279                 self._moveToPK(old_pk) 
     279                self.RowNumber = current_row 
    280280                raise dException.DBQueryException, e 
    281281            except dException.dException, e: 
    282282                if useTransact: 
    283283                    cursor.rollbackTransaction() 
    284                 self._moveToPK(old_pk) 
     284                self.RowNumber = current_row 
    285285                raise 
    286286 
     
    288288            cursor.commitTransaction() 
    289289 
    290         self.RowNumber = current_row 
     290        if current_row >= 0: 
     291            try: 
     292                self.RowNumber = current_row 
     293            except: pass 
    291294 
    292295 
     
    388391 
    389392        self.afterCancel() 
     393         
     394     
     395    def deleteAllChildren(self, startTransaction=False): 
     396        """Delete all children associated with the current record without 
     397        deleting the current record in this bizobj. 
     398        """ 
     399        cursor = self._CurrentCursor 
     400        errMsg = self.beforeDeleteAllChildren() 
     401        if errMsg: 
     402            raise dException.BusinessRuleViolation, errMsg 
     403 
     404        if startTransaction: 
     405            cursor.beginTransaction() 
     406 
     407        try: 
     408            for child in self.__children: 
     409                child.deleteAll(startTransaction=False) 
     410            if startTransaction: 
     411                cursor.commitTransaction() 
     412            self.afterDeleteAllChildren() 
     413 
     414        except dException.DBQueryException, e: 
     415            if startTransaction: 
     416                cursor.rollbackTransaction() 
     417            raise dException.DBQueryException, e 
     418        except StandardError, e: 
     419            if startTransaction: 
     420                cursor.rollbackTransaction() 
     421            raise StandardError, e 
    390422 
    391423 
     
    439471            if startTransaction: 
    440472                cursor.rollbackTransaction() 
    441             else: 
    442                 raise dException.DBQueryException, e 
     473            raise dException.DBQueryException, e 
    443474        except StandardError, e: 
    444             cursor.rollbackTransaction() 
     475            if startTransaction: 
     476                cursor.rollbackTransaction() 
    445477            raise StandardError, e 
    446478 
     
    650682            # sql passed; set it explicitly 
    651683            self.SQL = sql 
    652         # propagate the SQL downward: 
    653         self._CurrentCursor.setSQL(self.SQL) 
    654684 
    655685 
     
    716746        is built. 
    717747        """ 
    718         if self.DataSource and self.LinkField
     748        if self.DataSource and self.LinkField and self.Parent
    719749            if self.Parent.IsAdding: 
    720750                # Parent is new and not yet saved, so we cannot have child records yet. 
     
    759789        cursor's standard method for merging. 
    760790        """ 
     791        if not isinstance(params, tuple): 
     792            params = (params, ) 
    761793        self.__params = params 
    762794 
     
    967999        self.onNew() 
    9681000 
     1001        # Remove the memento for the new record, as we want to only record 
     1002        # changes made after this point. 
     1003        cursor._clearMemento() 
     1004 
    9691005 
    9701006    def onNew(self): 
    971         """ Hook method called after the default values have been set in onNew().""" 
    972         pass 
     1007        """Called when a new record is added.  
     1008 
     1009        Use this hook to add additional default field values, or anything else  
     1010        you need. If you change field values here, the memento system will not 
     1011        catch it (the record will not be marked 'dirty'). Use afterNew() if you 
     1012        instead want the memento system to record the changes. 
     1013        """ 
     1014    pass 
    9731015 
    9741016 
     
    10111053 
    10121054 
    1013     def addChildByRelationDict(self, dict, bizModule): 
    1014         """ Accepts a dictionary containing relationship information 
    1015         If any of the entries pertain to this bizobj, it will check to make 
    1016         sure that the child bizobj is already added, or add it and set the 
    1017         relationship if it isn't. It then passes the dict on to the child to 
    1018         allow the child to set up its relationships. 
    1019  
    1020         Returns a list containing all added child bizobjs. The list will 
    1021         be empty if none were added. 
    1022         """ 
     1055    def _addChildByRelationDict(self, dict, bizModule): 
     1056        """ Deprecated; used in old datanav framework.""" 
    10231057        addedChildren = [] 
    10241058        if self.__relationDictSet: 
     
    10561090                    for candidate, candidateClass in bizModule.__dict__.items(): 
    10571091                        if type(candidateClass) == type: 
    1058                             candidateInstance = candidateClass(self._conn
     1092                            candidateInstance = candidateClass(self._cursorFactory
    10591093                            if candidateInstance.DataSource.lower() == target.lower(): 
    10601094                                childBizClass = candidateClass 
    10611095 
    1062                     childBiz = childBizClass(self._conn
     1096                    childBiz = childBizClass(self._cursorFactory
    10631097                    self.addChild(childBiz) 
    10641098                    addedChildren.append(childBiz) 
     
    12471281 
    12481282 
     1283    def oldVal(self, fieldName, row=None): 
     1284        return self._CurrentCursor.oldVal(fieldName, row) 
     1285 
    12491286 
    12501287    ########## SQL Builder interface section ############## 
     
    12531290    def addFrom(self, exp): 
    12541291        return self._CurrentCursor.addFrom(exp) 
     1292    def addJoin(self, tbl, exp, joinType=None): 
     1293        return self._CurrentCursor.addJoin(tbl, exp, joinType) 
    12551294    def addGroupBy(self, exp): 
    12561295        return self._CurrentCursor.addGroupBy(exp) 
     
    12651304    def setFromClause(self, clause): 
    12661305        return self._CurrentCursor.setFromClause(clause) 
     1306    def setJoinClause(self, clause): 
     1307        return self._CurrentCursor.setJoinClause(clause) 
    12671308    def setGroupByClause(self, clause): 
    12681309        return self._CurrentCursor.setGroupByClause(clause) 
     1310    def getLimitClause(self): 
     1311        return self._CurrentCursor.getLimitClause() 
    12691312    def setLimitClause(self, clause): 
    12701313        return self._CurrentCursor.setLimitClause(clause) 
     1314    # For simplicity's sake, create aliases 
     1315    setLimit, getLimit = setLimitClause, getLimitClause 
    12711316    def setOrderByClause(self, clause): 
    12721317        return self._CurrentCursor.setOrderByClause(clause) 
     
    12751320    def prepareWhere(self, clause): 
    12761321        return self._CurrentCursor.prepareWhere(clause) 
    1277  
     1322    def getFieldClause(self): 
     1323        return self._CurrentCursor.getFieldClause() 
     1324    def getFromClause(self): 
     1325        return self._CurrentCursor.getFromClause() 
     1326    def getJoinClause(self): 
     1327        return self._CurrentCursor.getJoinClause() 
     1328    def getWhereClause(self): 
     1329        return self._CurrentCursor.getWhereClause() 
     1330    def getGroupByClause(self): 
     1331        return self._CurrentCursor.getGroupByClause() 
     1332    def getOrderByClause(self): 
     1333        return self._CurrentCursor.getOrderByClause() 
     1334    ########## END - SQL Builder interface section ############## 
    12781335 
    12791336 
     
    12831340    def beforeNew(self): return "" 
    12841341    def beforeDelete(self): return "" 
     1342    def beforeDeleteAllChildren(self): return "" 
    12851343    def beforeFirst(self): return "" 
    12861344    def beforePrior(self): return "" 
     
    12951353    def beforeCreateCursor(self): return "" 
    12961354    ########## Post-hook interface section ############## 
    1297     def afterNew(self): pass 
     1355    def afterNew(self):  
     1356        """Called after a new record is added.  
     1357 
     1358        Use this hook to change field values, or anything else you need. If you 
     1359        change field values here, the memento system will catch it. If you want 
     1360        to change field values and not trigger the memento system, use onNew() 
     1361        instead. 
     1362        """ 
     1363        pass 
    12981364    def afterDelete(self): pass 
     1365    def afterDeleteAllChildren(self): return "" 
    12991366    def afterFirst(self): pass 
    13001367    def afterPrior(self): pass 
     
    13271394        self._autoPopulatePK = bool(val) 
    13281395        if self._CurrentCursor: 
    1329             self._CurrentCursor.AutoPopulatePK= val 
     1396            self._CurrentCursor.AutoPopulatePK = val 
     1397 
     1398 
     1399    def _getAutoQuoteNames(self): 
     1400        return self._autoQuoteNames 
     1401 
     1402    def _setAutoQuoteNames(self, val): 
     1403        self._autoQuoteNames = val 
     1404        if self._CurrentCursor: 
     1405            self._CurrentCursor.AutoQuoteNames = val 
    13301406 
    13311407 
     
    14021478    def _setDefaultValues(self, val): 
    14031479        self._defaultValues = val 
     1480 
     1481 
     1482    def _getVirtualFields(self): 
     1483        # We need to save the explicitly-assigned VirtualFields here in the bizobj, 
     1484        # so that we are able to propagate it to any future-assigned child cursors. 
     1485        _df = getattr(self, "_virtualFields", None) 
     1486        if _df is not None: 
     1487            return _df 
     1488        return self._CurrentCursor.VirtualFields 
     1489 
     1490    def _setVirtualFields(self, val): 
     1491        for key, cursor in self.__cursors.items(): 
     1492            cursor.VirtualFields = val 
     1493        self._virtualFields = val 
    14041494 
    14051495 
     
    15991689    def _setSQL(self, val): 
    16001690        self._SQL = val 
    1601         cursor = self._CurrentCursor 
    1602         if cursor is not None: 
    1603             cursor.setSQL(val) 
    16041691 
    16051692 
     
    16081695            cursorClass = self._getCursorClass(self.dCursorMixinClass, 
    16091696                    self.dbapiCursorClass) 
    1610             cn = self._conn 
    1611             crs = self._sqlMgrCursor = cn.getCursor(cursorClass) 
    1612             crs.setCursorFactory(cn.getCursor, cursorClass) 
     1697            cf = self._cursorFactory 
     1698            crs = self._sqlMgrCursor = cf.getCursor(cursorClass) 
     1699            crs.setCursorFactory(cf.getCursor, cursorClass) 
    16131700            crs.KeyField = self.KeyField 
    16141701            crs.Table = self.DataSource 
    16151702            crs.AutoPopulatePK = self.AutoPopulatePK 
    1616             crs.BackendObject = cn.getBackendObject() 
     1703            crs.AutoQuoteNames = self.AutoQuoteNames 
     1704            crs.BackendObject = cf.getBackendObject() 
    16171705        return self._sqlMgrCursor 
    16181706 
     
    16371725            _("Determines if we are using a table that auto-generates its PKs. (bool)")) 
    16381726 
     1727    AutoQuoteNames = property(_getAutoQuoteNames, _setAutoQuoteNames, None, 
     1728            _("""When True (default), table and column names are enclosed with  
     1729            quotes during SQL creation in the cursor.  (bool)""")) 
     1730     
    16391731    AutoSQL = property(_getAutoSQL, None, None, 
    16401732            _("Returns the SQL statement automatically generated by the sql manager.")) 
     
    16781770    FillLinkFromParent = property(_getFillLinkFromParent, _setFillLinkFromParent, None, 
    16791771            _("""In the onNew() method, do we fill in the linkField with the value returned 
    1680             by calling the parent bizobj\'s GetKeyValue() method? (bool)""")) 
     1772            by calling the parent bizobj's GetKeyValue() method? (bool)""")) 
    16811773 
    16821774    IsAdding = property(_isAdding, None, None, 
     
    16971789 
    16981790    NewRecordOnNewParent = property(_getNewRecordOnNewParent, _setNewRecordOnNewParent, None, 
    1699             _("If this bizobj\'s parent has NewChildOnNew==True, do we create a record here? (bool)")) 
     1791            _("If this bizobj's parent has NewChildOnNew==True, do we create a record here? (bool)")) 
    17001792 
    17011793    NonUpdateFields = property(_getNonUpdateFields, _setNonUpdateFields, None, 
     
    17401832 
    17411833    SQL = property(_getSQL, _setSQL, None, 
    1742             _("SQL statement used to create the cursor\'s data. (str)")) 
     1834            _("SQL statement used to create the cursor's data. (str)")) 
    17431835 
    17441836    SqlManager = property(_getSqlMgr, None, None, 
     
    17471839    UserSQL = property(_getUserSQL, _setUserSQL, None, 
    17481840            _("SQL statement to run. If set, the automatic SQL builder will not be used.")) 
     1841 
     1842    VirtualFields = property(_getVirtualFields, _setVirtualFields, None, 
     1843            _("""A dictionary mapping virtual_field_name to function to call. 
     1844 
     1845            The specified function will be called when getFieldVal() is called on  
     1846            the specified virtual field name.""")) 
     1847 
  • branches/carl2/dabo/biz/test/test_dBizobj.py

    r2748 r2960  
    3838""" % locals()) 
    3939 
     40    def createNullRecord(self): 
     41        self.biz._CurrentCursor.AuxCursor.execute("""        
     42insert into %s (cField, iField, nField) values (NULL, NULL, NULL) 
     43""" % self.temp_table_name) 
    4044 
    4145    ## - Begin property unit tests - 
     
    7276                or ds[2] == ("iField", "G", False, self.temp_table_name, "iField", None)) 
    7377        self.assertEqual(ds[3], ("nField", "N", False, self.temp_table_name, "nField", None)) 
     78 
     79 
     80    def test_DefaultValues(self): 
     81        biz = self.biz 
     82        biz.DefaultValues["iField"] = 2342 
     83        biz.new() 
     84        self.assertEqual(biz.Record.iField, 2342) 
     85        self.assertEqual(biz.isChanged(), False)         
     86 
     87    def testVirtualFields(self): 
     88        biz = self.biz 
     89        def getCombinedName(): 
     90            return "%s:%s" % (biz.Record.cField.replace(" ", ""), biz.Record.iField) 
     91        biz.VirtualFields["combined_name"] = getCombinedName 
     92 
     93        def testBogus(): 
     94            return biz.Record.bogus_field_name 
     95 
     96        self.assertRaises(dabo.dException.FieldNotFoundException, testBogus) 
     97        self.assertEqual(biz.Record.combined_name, "PaulKeithMcNett:23") 
     98        biz.Record.combined_name = "shouldn't be able to set this" 
     99        self.assertEqual(biz.Record.combined_name, "PaulKeithMcNett:23") 
    74100 
    75101    def test_Encoding(self): 
     
    142168    ## - End property unit tests - 
    143169 
     170    ## - Begin method unit tests - 
     171 
     172    def test_cancel(self): 
     173        biz = self.biz 
     174        biz.Record.cField = "pkm" 
     175        biz.cancel() 
     176        self.assertEqual(biz.Record.cField, "Paul Keith McNett") 
     177        biz.new() 
     178        self.assertEqual(biz.RowCount, 4) 
     179        self.assertEqual(biz.RowNumber, 3) 
     180        biz.cancel() 
     181        self.assertEqual(biz.RowCount, 3) 
     182        self.assertEqual(biz.RowNumber, 2) 
     183 
     184    def test_isChanged(self): 
     185        biz = self.biz 
     186        self.assertEqual(biz.isChanged(), False) 
     187        biz.Record.cField = "The Magnificent Seven" 
     188        self.assertEqual(biz.isChanged(), True) 
     189        biz.cancel() 
     190        self.assertEqual(biz.isChanged(), False) 
     191 
     192        # isChanged()   should be False for new records that haven't had any field 
     193        # value changes.     
     194        biz.new() 
     195        self.assertEqual(biz.isChanged(), False) 
     196        biz.Record.cField = "Hitsville U.K." 
     197        self.assertEqual(biz.isChanged(), True) 
     198 
     199 
     200    def test_oldVal(self): 
     201        biz = self.biz 
     202        self.assertEqual(biz.oldVal("cField"), biz.Record.cField) 
     203        self.assertEqual(biz.oldVal("cField", 1), biz.getFieldVal("cField", 1)) 
     204        oldVal = biz.Record.cField  
     205        newVal = "pkm23" 
     206        biz.Record.cField = newVal 
     207        self.assertEqual(biz.oldVal("cField"), oldVal) 
     208        self.assertEqual(biz.Record.cField, newVal) 
     209        self.assertRaises(dabo.dException.FieldNotFoundException, biz.oldVal, "bogusField") 
     210 
     211    ## - End method unit tests - 
     212 
     213    def testDeleteNewSave(self): 
     214        biz = self.biz 
     215        biz.delete() 
     216        biz.new() 
     217        biz.save() 
     218        self.assertEqual(biz.RowCount, 3) 
     219        self.assertEqual(biz.RowNumber, 2) 
     220 
     221        biz.deleteAll() 
     222        self.assertEqual(biz.RowCount, 0) 
     223 
     224        self.assertRaises(dabo.dException.NoRecordsException, biz.save) 
     225        self.assertRaises(dabo.dException.NoRecordsException, biz.delete) 
     226 
     227        biz.new() 
     228        self.assertEqual(biz.RowCount, 1) 
     229        biz.delete() 
     230        self.assertEqual(biz.RowCount, 0) 
     231        self.assertRaises(dabo.dException.NoRecordsException, biz.save) 
     232 
     233        biz.new() 
     234        biz.save() 
     235        self.assertEqual(biz.RowCount, 1) 
     236        self.assertEqual(biz.RowNumber, 0) 
     237 
    144238 
    145239    def testMementos(self): 
     
    175269        self.assertEqual(biz.RowNumber, 3) 
    176270        self.assertEqual(cur._newRecords, {"-1-dabotmp": None}) 
    177         self.assertEqual(biz.isChanged(), True
     271        self.assertEqual(biz.isChanged(), False)  ## (because no field changed in new record
    178272        self.assertEqual(cur.Record.pk, "-1-dabotmp") 
    179273        self.assertEqual(biz.Record.cField, "") 
     
    181275        self.assertEqual(biz.Record.nField, 0) 
    182276        biz.save() 
     277        self.assertEqual(biz.RowCount, 4)  ## still have 4 rows, even though the last one wasn't saved 
    183278        biz.requery() 
    184         self.assertEqual(biz.RowCount, 4) 
    185         self.assertEqual(biz.RowNumber, 3) 
     279        # We only have 3 rows, because one of the prior 4 rows was new with no changed fields: 
     280        self.assertEqual(biz.RowCount, 3) 
     281        # ...and RowNumber went to 0, because the previous row number (3) doesn't exist anymore: 
     282        self.assertEqual(biz.RowNumber, 0) 
    186283        self.assertEqual(cur._newRecords, {}) 
    187284        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) 
     285        self.assertEqual(biz.Record.pk, 1) 
     286 
    194287 
    195288    def testChildren(self): 
     
    206299 
    207300        # At this point bizMain should be at row 0, and bizChild should have 
    208         # two records, and be on row 0 
     301        # two records, and be on row 0, and isChanged() should be False, since 
     302        # no fields have been updated. 
    209303        self.assertEqual(bizMain.Record.pk, 1) 
    210304        self.assertEqual(bizMain.RowNumber, 0) 
     
    213307        self.assertEqual(bizChild.Record.pk, 1) 
    214308        self.assertEqual(bizChild.Record.parent_fk, 1) 
     309        self.assertEqual(bizMain.isChanged(), False) 
     310        self.assertEqual(bizChild.isChanged(), False) 
     311        self.assertEqual(bizMain.isAnyChanged(), False) 
     312        self.assertEqual(bizChild.isAnyChanged(), False) 
    215313 
    216314        bizMain.next() 
     
    259357        self.assertEqual(bizChild.IsAdding, True) 
    260358        self.assertEqual(bizChild.RowCount, 1) 
     359        self.assertEqual(bizChild.isAnyChanged(), False) 
     360        self.assertEqual(bizChild.isChanged(), False) 
     361        self.assertEqual(bizMain.isAnyChanged(), False) 
     362        self.assertEqual(bizMain.isChanged(), False) 
     363        bizChild.Record.cInvNum = "IN99991" 
     364        self.assertEqual(bizMain.isChanged(), True) 
     365        self.assertEqual(bizMain.isAnyChanged(), True) 
     366        self.assertEqual(bizChild.isChanged(), True) 
    261367        self.assertEqual(bizChild.isAnyChanged(), True) 
    262         self.assertEqual(bizChild.isChanged(), True) 
    263         self.assertEqual(bizMain.isAnyChanged(), True) 
    264         self.assertEqual(bizMain.isChanged(), True) 
    265         bizChild.Record.cInvNum = "IN99991" 
    266368 
    267369        bizMain.prior() 
     
    296398        # Test the case where you add a new parent record but no new children: 
    297399        bizMain.new() 
     400        bizMain.Record.cField = "Junco Pardner" 
    298401        self.assertEqual(bizMain.RowCount, 4) 
    299402        self.assertEqual(bizMain.RowNumber, 3) 
     
    305408        self.assertEqual(bizMain.RowNumber, 3) 
    306409 
     410    def testNullRecord(self): 
     411        biz = self.biz 
     412        self.createNullRecord() 
     413