dabo Commit
Revision 6595
Date: 2011-05-25 02:57:43 -0700 (Wed, 25 May 2011)
Author: Jacekk
Trac: http://trac.dabodev.com/changeset/6595
Changed:
U trunk/dabo/biz/dBizobj.py
Log:
This fairly huge commit is the first attempt to fix Dabo issue
with detecting modified children bizobjs.
It should fix #1390 ticket.
Quick list of changes:
- the saveAll no longer uses scanChangedRows
- the validate method isn't called if only the child bizobj is changed
- data changes detection is moved from the validate to the save method
- the scan method now return value from the last called function (useful for
detecting changed rows)
- redundant exception logging in nested scans are avoided.
Although I'm testing this code since last december, there still can be
unexpected issues.
Please, report them immediately.
Diff:
Modified: trunk/dabo/biz/dBizobj.py
===================================================================
--- trunk/dabo/biz/dBizobj.py 2011-05-25 08:01:46 UTC (rev 6594)
+++ trunk/dabo/biz/dBizobj.py 2011-05-25 09:57:43 UTC (rev 6595)
@@ -14,6 +14,8 @@
NO_RECORDS_PK = "75426755-2f32-4d3d-86b6-9e2a1ec47f2c" ## Can't use None
+# To filter logging noise in scan methods, identify the redundant exceptions.
+_scanExceptionId = None
@@ -402,44 +404,29 @@
def saveAll(self, startTransaction=True):
- """Saves all changes to the bizobj and children."""
+ """
+ Saves all changes to the bizobj and children.
+ """
+ # JKA: I can't see any sense in using 'scanChangedRows()' here,
since
+ # we must check for changes in 'save()' method too.
+ if not self.RowCount:
+ # If there are no records, there can be no changes
+ return
rp = self._RemoteProxy
if rp:
return rp.saveAll(startTransaction=startTransaction)
- cursorKey = self.__currentCursorKey
- current_row = self.RowNumber
startTransaction = startTransaction and self.beginTransaction()
try:
- self.scanChangedRows(self.save,
includeNewUnchanged=self.SaveNewUnchanged,
- startTransaction=False)
+ self.scan(self.save, startTransaction=False,
scanRequeryChildren=False)
if startTransaction:
self.commitTransaction()
-
except dException.ConnectionLostException:
- self._CurrentCursor = cursorKey
- self.RowNumber = current_row
- raise
- except dException.DBQueryException:
+ pass
+ except (dException.DBQueryException, dException.dException):
# Something failed; reset things.
if startTransaction:
self.rollbackTransaction()
- self._CurrentCursor = cursorKey
- self.RowNumber = current_row
- # Pass the exception to the UI
raise
- except dException.dException:
- if startTransaction:
- self.rollbackTransaction()
- self._CurrentCursor = cursorKey
- self.RowNumber = current_row
- raise
- self._CurrentCursor = cursorKey
- if current_row >= 0:
- try:
- self.RowNumber = current_row
- except StandardError, e:
- # Need to record what sort of error could be
thrown
- dabo.log.error(_("Failed to set RowNumber.
Error: %s") % ustr(e))
def save(self, startTransaction=True, saveTheChildren=True):
@@ -449,66 +436,67 @@
If the save is successful, the saveAll() of all child bizobjs
will be
called as well if saveTheChildren is True (the default).
"""
+ if not self.RowCount:
+ # If there are no records, there can be no changes
+ return
+ if not
self.isChanged(includeNewUnchanged=self.SaveNewUnchanged):
+ return
rp = self._RemoteProxy
if rp:
return rp.save(startTransaction=startTransaction)
+
+ # Check if current data set is changed.
cursor = self._CurrentCursor
errMsg = self.beforeSave()
+
if errMsg:
raise dException.BusinessRuleViolation(errMsg)
-
if self.KeyField is None:
raise dException.MissingPKException(
- _("No key field defined for table: ") +
self.DataSource)
+ _("No key field defined for table: %s")
% self.DataSource)
- # Validate any changes to the data. If there is data that fails
- # validation, an Exception will be raised.
- self._validate()
-
- startTransaction = startTransaction and self.beginTransaction()
+ # Check if only this row is changed.
+ isRowChanged = self.__areThereAnyChanges
# Save to the Database, but first save the IsAdding flag as the
save() call
# will reset it to False:
isAdding = self.IsAdding
+
+ if isRowChanged or isAdding:
+ # Validate row.
+ self._validate()
+
+ startTransaction = startTransaction and self.beginTransaction()
+
try:
- cursor.save(includeNewUnchanged=self.SaveNewUnchanged)
- if isAdding:
- # Call the hook method for saving new records.
- self._onSaveNew()
+ # Maybe this record isn't changed but some children are.
+ if isRowChanged or isAdding:
+ # Save cursor data.
+ cursor.save(includeNewUnchanged=True)
+ if isAdding:
+ # Call the hook method for saving new
records.
+ self._onSaveNew()
if saveTheChildren:
# Iterate through the child bizobjs, telling
them to save themselves.
for child in self.__children:
# No need to start another transaction.
And since this is a child bizobj,
# we need to save all rows that have
changed.
- if child.RowCount > 0:
-
child.saveAll(startTransaction=False)
+ child.saveAll(startTransaction=False)
# Finish the transaction, and requery the children if
needed.
if startTransaction:
self.commitTransaction()
if self.RequeryChildOnSave:
self.requeryAllChildren()
-
- except dException.ConnectionLostException:
+ except (dException.ConnectionLostException,
dException.NoRecordsException):
raise
-
- except dException.NoRecordsException:
- raise
-
- except dException.DBQueryException:
+ except (dException.DBQueryException, dException.dException):
# Something failed; reset things.
if startTransaction:
self.rollbackTransaction()
# Pass the exception to the UI
raise
- except dException.dException:
- # Something failed; reset things.
- if startTransaction:
- self.rollbackTransaction()
- # Pass the exception to the UI
- raise
-
# Two hook methods: one specific to Save(), and one which is
called after any change
# to the data (either save() or delete()).
self.afterChange()
@@ -521,7 +509,7 @@
and all new, unmodified records.
"""
self.scanChangedRows(self.cancel, allCursors=False,
includeNewUnchanged=True,
- ignoreNoRecords=ignoreNoRecords)
+ ignoreNoRecords=ignoreNoRecords, reverse=True)
def cancel(self, ignoreNoRecords=None):
@@ -705,11 +693,19 @@
record that has not been modified from its defaults will
suffice to mark the
record as changed.
"""
+ if not self.RowCount:
+ # If there are no records, there can be no changes
+ return []
if self.__children:
+ rows = []
+
+ def _isRowChanged():
+ if self.isChanged(includeNewUnchanged):
+ rows.append(self.RowNumber)
+
# Must iterate all records to find potential changes in
children:
- self.__changedRows = []
- self.scan(self._listChangedRows, includeNewUnchanged)
- return self.__changedRows
+ self.scan(_isRowChanged, scanRequeryChildren=False)
+ return rows
else:
# Can use the much faster cursor.getChangedRows():
return
self._CurrentCursor.getChangedRows(includeNewUnchanged)
@@ -783,8 +779,13 @@
order, which you'll want to do if, for example, you are deleting
records in your scan function. If the reverse argument is not
sent,
self.ScanReverse will be queried to determine the behavior.
+
+ Returns value from 'func' called in the last iteration.
"""
- self.scanRows(func, range(self.RowCount), *args, **kwargs)
+ rowCount = self.RowCount
+ if not rowCount > 0:
+ return
+ return self.scanRows(func, range(rowCount), *args, **kwargs)
def scanRows(self, func, rows, *args, **kwargs):
@@ -806,42 +807,29 @@
del(kwargs["scanRequeryChildren"])
except KeyError:
requeryChildren = self.ScanRequeryChildren
- try:
- currPK = self.getPK()
- currRow = None
- except dException.dException:
- # No PK defined
- currPK = None
- currRow = self.RowNumber
+ currentStatus = self.__getCurrentStatus()
+ ret = None
- def restorePosition():
- if self.ScanRestorePosition:
- if currPK is not None:
- self._positionUsingPK(currPK,
updateChildren=False)
- else:
- try:
- self.RowNumber = currRow
- except StandardError, e:
- # Perhaps the row was deleted;
at any rate, leave the pointer
- # at the end of the data set
- row = self.RowCount - 1
- if row >= 0:
- self.RowNumber = row
- requeryChildren = self.ScanRequeryChildren
try:
if reverse:
rows.reverse()
for i in rows:
self._moveToRowNum(i,
updateChildren=requeryChildren)
- func(*args, **kwargs)
+ ret = func(*args, **kwargs)
if self.exitScan:
break
- except Exception:
- restorePosition()
+ except Exception, e:
+ if self._logScanException(e):
+ dabo.log.error(_("Error in scanRows of %s: %s")
% (self.Name, ustr(e)))
+ if self.ScanRestorePosition:
+ self.__setCurrentStatus(currentStatus)
raise
- restorePosition()
+ if self.ScanRestorePosition:
+ self.__setCurrentStatus(currentStatus)
+ return ret
+
def scanChangedRows(self, func, allCursors=False,
includeNewUnchanged=False,
*args, **kwargs):
"""
@@ -858,39 +846,40 @@
Records are scanned in arbitrary order. Any exception raised by
calling
func() will be passed up to the caller.
"""
- self.exitScan = False
- currCursor = self._CurrentCursor
- old_currentCursorKey = self.__currentCursorKey
- try:
- old_pk = currCursor.getPK()
- except dException.NoRecordsException:
- # no rows to scan
+ if not self.RowCount:
return
-
+ currentStatus = self.__getCurrentStatus()
if allCursors:
- cursors = self.__cursors
+ cursors = self._cursorDictReference()
else:
- cursors = {old_currentCursorKey: currCursor}
+ cursors = {currentStatus[0]: self._CurrentCursor}
- for key, cursor in cursors.iteritems():
- self._CurrentCursor = key
- changedRows = self.getChangedRows(includeNewUnchanged)
- for row in sorted(changedRows, reverse=True):
- self._moveToRowNum(row, updateChildren=False)
- try:
- func(*args, **kwargs)
- except StandardError, e:
- # Reset things and bail
- dabo.log.error(_("Error in
scanChangedRows: %s") % ustr(e))
- self._CurrentCursor =
old_currentCursorKey
- self._positionUsingPK(old_pk,
updateChildren=False)
- raise
+ try:
+ reverse = kwargs["reverse"]
+ del(kwargs["reverse"])
+ except KeyError:
+ reverse = self.ScanReverse
- self._CurrentCursor = old_currentCursorKey
- if old_pk is not None:
- self._positionUsingPK(old_pk, updateChildren=False)
+ def _callFunc():
+ if self.isChanged(includeNewUnchanged):
+ return func(*args, **kwargs)
+ ret = None
+ try:
+ for key in cursors:
+ self._CurrentCursor = key
+ ret = self.scan(_callFunc, reverse=reverse,
scanRequeryChildren=False)
+ except Exception, e:
+ if self._logScanException(e):
+ dabo.log.error(_("Error in scanChangedRows of
%s: %s") % (self.Name, ustr(e)))
+ self.__setCurrentStatus(currentStatus)
+ raise
+
+ self.__setCurrentStatus(currentStatus)
+ return ret
+
+
def getFieldNames(self):
"""Returns a tuple of all the field names in the cursor."""
rp = self._RemoteProxy
@@ -919,6 +908,62 @@
return ret
+ def _logScanException(self, ex):
+ """
+ For internal use only. For nested scan loop, returns False if
exception
+ was already raised in the loop.
+ """
+ global _scanExceptionId
+ exHash = hash(ex)
+ if _scanExceptionId <> exHash:
+ _scanExceptionId = exHash
+ return True
+ else:
+ return False
+
+
+ def __getCurrentStatus(self):
+ """
+ Returns current dataset status (CursorKey, PK value, RowNumber)
tuple.
+ """
+ try:
+ currPK = self.getPK()
+ except dException.dException:
+ # No PK defined
+ currPK = None
+ return self._CurrentCursorKey, currPK, self.RowNumber
+
+
+ def __setCurrentStatus(self, status, restoreCursor=True):
+ """
+ Set current dataset status on the basis of status tuple.
+ """
+ cursors = self._cursorDictReference()
+ if restoreCursor:
+ if status[0] in cursors:
+ self._CurrentCursor = status[0]
+ else:
+ # Maybe key changed after save...
+ pass
+ rowCnt = self.RowCount
+ if rowCnt:
+ if status[1] is not None:
+ self._positionUsingPK(status[1], False)
+ if status[1] is None or status[1] <> self.getPK():
+ try:
+ self._moveToRowNum(status[2], False)
+ except StandardError, e:
+ # Perhaps the row was deleted; at any
rate, leave the pointer
+ # at the end of the data set
+ row = rowCnt - 1
+ if row >= 0:
+ self._moveToRowNum(row, False)
+ else:
+ dabo.log.error(
+ _("Failed to set
RowNumber of %s: %s") % \
+ (self.Name,
ustr(e)))
+
+
def replace(self, field, valOrExpr, scope=None):
"""
Replaces the value of the specified field with the given value
@@ -1274,11 +1319,9 @@
save() will not be allowed to proceed.
"""
errMsg = ""
- if self.isChanged():
- # No need to validate if the data hasn't changed
- message = self.validateRecord()
- if message:
- errMsg += message
+ message = self.validateRecord()
+ if message:
+ errMsg += message
if errMsg:
raise dException.BusinessRuleViolation(errMsg)
@@ -1438,29 +1481,49 @@
def isAnyChanged(self, useCurrentParent=None, includeNewUnchanged=None):
- """Returns True if any record in the current record set has
been changed."""
+ """
+ Returns True if any record in the current record set has been
changed.
+ """
+ if useCurrentParent:
+ oldKey = self._CurrentCursorKey
+ crsKey = self.getParentLinkValue()
+ crs = self._cursorDictReference().get(crsKey, None)
+ if crs is None or not crs.RowCount:
+ # No cursor or records, no changes.
+ return False
+ self._CurrentCursor = crsKey
+ if not self.RowCount:
+ return False
if includeNewUnchanged is None:
includeNewUnchanged = self.SaveNewUnchanged
- if useCurrentParent is None:
- cc = self._CurrentCursor
- else:
- key = self.getParentLinkValue()
- cc = self.__cursors.get(key, None)
+
+ def _isThisChanged():
+ self.exitScan = self.isChanged(includeNewUnchanged)
+ return self.exitScan
+
+ ret = self.scan(_isThisChanged, scanRequeryChildren=False)
+ if useCurrentParent:
+ self._CurrentCursor = oldKey
+ return ret
+
+
+ def isRowChanged(self, includeNewUnchanged=None):
+ """
+ Return True if data has changed in the current and only current
row
+ of this bizobj, without any children.
+ """
+ if not self.RowCount:
+ # If there are no records, there can be no changes
+ return False
+ cc = self._CurrentCursor
if cc is None:
# No cursor, no changes.
return False
+ if includeNewUnchanged is None:
+ includeNewUnchanged = self.SaveNewUnchanged
+ return cc.isChanged(allRows=False,
includeNewUnchanged=includeNewUnchanged)
- if cc.isChanged(allRows=True,
includeNewUnchanged=includeNewUnchanged):
- return True
- # Nothing's changed in the top level, so we need to recurse the
children:
- for child in self.__children:
- if
child.isAnyChanged(useCurrentParent=useCurrentParent,
includeNewUnchanged=includeNewUnchanged):
- return True
- # If we made it to here, there are no changes.
- return False
-
-
def isChanged(self, includeNewUnchanged=None):
"""
Return True if data has changed in this bizobj and any children.
@@ -1468,21 +1531,15 @@
By default, only the current record is checked. Call
isAnyChanged() to
check all records.
"""
- if includeNewUnchanged is None:
- includeNewUnchanged = self.SaveNewUnchanged
- cc = self._CurrentCursor
- if cc is None:
- # No cursor, no changes.
+ if not self.RowCount:
+ # If there are no records, there can be no changes.
return False
- ret = cc.isChanged(allRows=False,
includeNewUnchanged=includeNewUnchanged)
+ self.__areThereAnyChanges = ret =
self.isRowChanged(includeNewUnchanged=includeNewUnchanged)
+
if not ret:
- # see if any child bizobjs have changed
- if not self.RowCount:
- # If there are no records, there can be no
changes
- return False
- for child in self.__children:
- ret = child.isAnyChanged(useCurrentParent=True,
includeNewUnchanged=includeNewUnchanged)
+ for child in self.getChildren():
+ ret =
child.isAnyChanged(includeNewUnchanged=includeNewUnchanged)
if ret:
break
return ret
@@ -2132,7 +2189,7 @@
########## Post-hook interface section ##############
afterNew = _makeHookMethod("afterNew", "a new record is added",
- additionalDoc = \
+ additionalDoc=\
"""Use this hook to change field values of newly added records. If
you change field values here, the memento system will catch it and
prompt you to save if needed later on. If you want to change field
_______________________________________________
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]