Ticket #1099: dBizobj.py

File dBizobj.py, 60.7 kB (added by ed, 11 months ago)

dBizobj modified with debugging output

Line 
1 # -*- coding: utf-8 -*-
2 import types
3 import re
4 import dabo
5 import dabo.dConstants as kons
6 from dabo.db.dCursorMixin import dCursorMixin
7 from dabo.dLocalize import _
8 import dabo.dException as dException
9 from dabo.dObject import dObject
10
11
12 NO_RECORDS_PK = "75426755-2f32-4d3d-86b6-9e2a1ec47f2c"  ## Can't use None
13
14
15 class dBizobj(dObject):
16     """ The middle tier, where the business logic resides."""
17     # Class to instantiate for the cursor object
18     dCursorMixinClass = dCursorMixin
19     # Tell dObject that we'll call before and afterInit manually:
20     _call_beforeInit, _call_afterInit, _call_initProperties = False, False, False
21
22
23     def debug(self):
24         return self.DataSource == "FotoInf"
25
26
27
28     def __init__(self, conn=None, properties=None, *args, **kwargs):
29         """ User code should override beforeInit() and/or afterInit() instead."""
30         self.__att_try_setFieldVal = False
31         # Collection of cursor objects. MUST be defined first.
32         self.__cursors = {}
33         # PK of the currently-selected cursor
34         self.__currentCursorKey = None
35         self._dataStructure = None
36
37         # Dictionary holding any default values to apply when a new record is created. This is
38         # now the DefaultValues property (used to be self.defaultValues attribute)
39         self._defaultValues = {}
40
41         self._beforeInit()
42         self.setConnection(conn)
43         # We need to make sure the cursor is created *before* the call to
44         # initProperties()
45         self._initProperties()
46         super(dBizobj, self).__init__(properties=properties, *args, **kwargs)
47         self._afterInit()
48         self.__att_try_setFieldVal = True
49
50
51     def _beforeInit(self):
52         # Cursor to manage SQL Builder info.
53         self._sqlMgrCursor = None
54         self._cursorFactory = None
55         self.__params = ()      # tuple of params to be merged with the sql in the cursor
56         self.__children = []        # Collection of child bizobjs
57         self._baseClass = dBizobj
58         self.__areThereAnyChanges = False   # Used by the isChanged() method.
59         # Used by the LinkField property
60         self._linkField = ""
61         self._parentLinkField = ""
62         # Used the the _addChildByRelationDict() method to eliminate infinite loops
63         self.__relationDictSet = False
64         # Do we try to same on the same record during a requery?
65         self._restorePositionOnRequery = True
66
67         # Various attributes used for Properties
68         self._caption = ""
69         self._dataSource = ""
70         self._nonUpdateFields = []
71         self._scanRestorePosition = True
72         self._scanReverse = False
73         self._SQL = ""
74         self._userSQL = None
75         self._requeryOnLoad = False
76         self._parent  = None
77         self._autoPopulatePK = True
78         self._autoQuoteNames = True
79         self._keyField = ""
80         self._requeryChildOnSave = False
81         self._newRecordOnNewParent = False
82         self._newChildOnNew = False
83         self._fillLinkFromParent = False
84         self.exitScan = False
85
86         ##########################################
87         ### referential integrity stuff ####
88         ##########################################
89         ### Possible values for each type (not all are relevant for each action):
90         ### IGNORE - don't worry about the presence of child records
91         ### RESTRICT - don't allow action if there are child records
92         ### CASCADE - changes to the parent are cascaded to the children
93         self.deleteChildLogic = kons.REFINTEG_CASCADE  # child records will be deleted
94         self.updateChildLogic = kons.REFINTEG_IGNORE   # parent keys can be changed w/o
95                                                     # affecting children
96         self.insertChildLogic = kons.REFINTEG_IGNORE   # child records can be inserted
97                                                     # even if no parent record exists.
98         ##########################################
99
100         self.beforeInit()
101
102
103     def setConnection(self, conn):
104         """Normally connections are established before bizobj creation, but
105         for those cases where connections are created later, use this method to
106         establish the connection used by the bizobj.
107         """
108         self._cursorFactory = conn
109         if conn:
110             # Base cursor class : the cursor class from the db api
111             self.dbapiCursorClass = self._cursorFactory.getDictCursorClass()
112             # If there are any problems in the createCursor process, an
113             # exception will be raised in that method.
114             self.createCursor()
115
116
117     def getTempCursor(self):
118         """Occasionally it is useful to be able to run ad-hoc queries against
119         the database. For these queries, where the results are not meant to
120         be managed as in regular bizobj/cursor relationships, a temp cursor
121         will allow you to run those queries, get the results, and then dispose
122         of the cursor.
123         """
124         cf = self._cursorFactory
125         cursorClass = self._getCursorClass(self.dCursorMixinClass,
126                 self.dbapiCursorClass)
127         crs = cf.getCursor(cursorClass)
128         crs.BackendObject = cf.getBackendObject()
129         crs.setCursorFactory(cf.getCursor, cursorClass)
130         return crs
131
132
133     def createCursor(self, key=None):
134         """ Create the cursor that this bizobj will be using for data, and store it
135         in the dictionary for cursors, with the passed value of 'key' as its dict key.
136         For independent bizobjs, that key will be None.
137
138         Subclasses should override beforeCreateCursor() and/or afterCreateCursor()
139         instead of overriding this method, if possible. Returning any non-empty value
140         from beforeCreateCursor() will prevent the rest of this method from
141         executing.
142         """
143         if self.__cursors:
144             _dataStructure = getattr(self.__cursors[self.__cursors.keys()[0]], "_dataStructure", None)
145             self._virtualFields = getattr(self.__cursors[self.__cursors.keys()[0]], "_virtualFields", {})
146         else:
147             # The first cursor is being created, before DataStructure is assigned.
148             _dataStructure = None
149             self._virtualFields = {}
150         errMsg = self.beforeCreateCursor()
151         if errMsg:
152             raise dException.dException, errMsg
153
154         cursorClass = self._getCursorClass(self.dCursorMixinClass,
155                 self.dbapiCursorClass)
156
157         if key is None:
158             key = self.__currentCursorKey
159
160         cf = self._cursorFactory
161         self.__cursors[key] = cf.getCursor(cursorClass)
162         self.__cursors[key].setCursorFactory(cf.getCursor, cursorClass)
163
164         crs = self.__cursors[key]
165         if _dataStructure is not None:
166             crs._dataStructure = _dataStructure
167         crs._virtualFields = self._virtualFields
168         crs.KeyField = self.KeyField
169         crs.Table = self.DataSource
170         crs.AutoPopulatePK = self.AutoPopulatePK
171         crs.AutoQuoteNames = self.AutoQuoteNames
172         crs.BackendObject = cf.getBackendObject()
173         crs.sqlManager = self.SqlManager
174         crs.UserSQL = self.UserSQL
175         crs._bizobj = self
176         if self.RequeryOnLoad:
177             crs.requery()
178             self.first()
179         self.afterCreateCursor(crs)
180
181
182     def _getCursorClass(self, main, secondary):
183         class cursorMix(main, secondary):
184             superMixin = main
185             superCursor = secondary
186             def __init__(self, *args, **kwargs):
187                 if hasattr(main, "__init__"):
188                     apply(main.__init__,(self,) + args, kwargs)
189                 if hasattr(secondary, "__init__"):
190                     apply(secondary.__init__,(self,) + args, kwargs)
191         return  cursorMix
192
193
194     def first(self):
195         """ Move to the first record of the data set.
196
197         Any child bizobjs will be requeried to reflect the new parent record. If
198         there are no records in the data set, an exception will be raised.
199         """
200         errMsg = self.beforeFirst()
201         if not errMsg:
202             errMsg = self.beforePointerMove()
203         if errMsg:
204             raise dException.BusinessRuleViolation, errMsg
205
206         self._CurrentCursor.first()
207         self.requeryAllChildren()
208
209         self.afterPointerMove()
210         self.afterFirst()
211
212
213     def prior(self):
214         """ Move to the prior record of the data set.
215
216         Any child bizobjs will be requeried to reflect the new parent record. If
217         there are no records in the data set, an exception will be raised.
218         """
219         errMsg = self.beforePrior()
220         if not errMsg:
221             errMsg = self.beforePointerMove()
222         if errMsg:
223             raise dException.BusinessRuleViolation, errMsg
224
225         self._CurrentCursor.prior()
226         self.requeryAllChildren()
227
228         self.afterPointerMove()
229         self.afterPrior()
230
231
232     def next(self):
233         """ Move to the next record of the data set.
234
235         Any child bizobjs will be requeried to reflect the new parent record. If
236         there are no records in the data set, an exception will be raised.
237         """
238         errMsg = self.beforeNext()
239         if not errMsg:
240             errMsg = self.beforePointerMove()
241         if errMsg:
242             raise dException.BusinessRuleViolation, errMsg
243
244         self._CurrentCursor.next()
245         self.requeryAllChildren()
246
247         self.afterPointerMove()
248         self.afterNext()
249
250
251     def last(self):
252         """ Move to the last record of the data set.
253
254         Any child bizobjs will be requeried to reflect the new parent record. If
255         there are no records in the data set, an exception will be raised.
256         """
257         errMsg = self.beforeLast()
258         if not errMsg:
259             errMsg = self.beforePointerMove()
260         if errMsg:
261             raise dException.BusinessRuleViolation, errMsg
262
263         self._CurrentCursor.last()
264         self.requeryAllChildren()
265
266         self.afterPointerMove()
267         self.afterLast()
268
269
270     def saveAll(self, startTransaction=True):
271         """Saves all changes to the bizobj and children."""
272         cursor = self._CurrentCursor
273         current_row = self.RowNumber
274         app = self.Application
275         isTransactionManager = False
276         if startTransaction:
277             isTransactionManager = app.getTransactionToken(self)
278             if isTransactionManager:
279                 cursor.beginTransaction()
280
281         try:
282             self.scanChangedRows(self.save, includeNewUnchanged=self.SaveNewUnchanged,
283                     startTransaction=False)
284             if isTransactionManager:
285                 cursor.commitTransaction()
286                 app.releaseTransactionToken(self)
287
288         except dException.ConnectionLostException, e:
289             self.RowNumber = current_row
290             raise dException.ConnectionLostException, e
291         except dException.DBQueryException, e:
292             # Something failed; reset things.
293             if isTransactionManager:
294                 cursor.rollbackTransaction()
295                 app.releaseTransactionToken(self)
296             # Pass the exception to the UI
297             self.RowNumber = current_row
298             raise dException.DBQueryException, e
299         except dException.dException, e:
300             if isTransactionManager:
301                 cursor.rollbackTransaction()
302                 app.releaseTransactionToken(self)
303             self.RowNumber = current_row
304             raise
305
306         if current_row >= 0:
307             try:
308                 self.RowNumber = current_row
309             except: pass
310
311
312     def save(self, startTransaction=True):
313         """Save any changes that have been made in the current row.
314
315         If the save is successful, the saveAll() of all child bizobjs will be
316         called as well.
317         """
318         cursor = self._CurrentCursor
319         errMsg = self.beforeSave()
320         if errMsg:
321             raise dException.BusinessRuleViolation, errMsg
322
323         if self.KeyField is None:
324             raise dException.MissingPKException, _("No key field defined for table: ") + self.DataSource
325
326         # Validate any changes to the data. If there is data that fails
327         # validation, an Exception will be raised.
328         self._validate()
329
330         app = self.Application
331         isTransactionManager = False
332         if startTransaction:
333             isTransactionManager = app.getTransactionToken(self)
334             if isTransactionManager:
335                 cursor.beginTransaction()
336
337         # Save to the Database, but first save the IsAdding flag as the save() call
338         # will reset it to False:
339         isAdding = self.IsAdding
340         try:
341             cursor.save()
342             if isAdding:
343                 # Call the hook method for saving new records.
344                 self._onSaveNew()
345
346             # Iterate through the child bizobjs, telling them to save themselves.
347             for child in self.__children:
348                 # No need to start another transaction. And since this is a child bizobj,
349                 # we need to save all rows that have changed.
350                 if child.RowCount > 0:
351                     child.saveAll(startTransaction=False)
352
353             # Finish the transaction, and requery the children if needed.
354             if isTransactionManager:
355                 cursor.commitTransaction()
356                 app.releaseTransactionToken(self)
357             if self.RequeryChildOnSave:
358                 self.requeryAllChildren()
359
360         except dException.ConnectionLostException, e:
361             raise
362
363         except dException.NoRecordsException, e:
364             raise
365
366         except dException.DBQueryException, e:
367             # Something failed; reset things.
368             if isTransactionManager:
369                 cursor.rollbackTransaction()
370                 app.releaseTransactionToken(self)
371             # Pass the exception to the UI
372             raise dException.DBQueryException, e
373
374         except dException.dException, e:
375             # Something failed; reset things.
376             if isTransactionManager:
377                 cursor.rollbackTransaction()
378                 app.releaseTransactionToken(self)
379             # Pass the exception to the UI
380             raise
381
382         # Two hook methods: one specific to Save(), and one which is called after any change
383         # to the data (either save() or delete()).
384         self.afterChange()
385         self.afterSave()
386
387
388     def cancelAll(self, ignoreNoRecords=None):
389         """Cancel all changes made to the current dataset, including all children
390         and all new, unmodified records.
391         """
392         self.scanChangedRows(self.cancel, allCursors=False, includeNewUnchanged=True,
393                 ignoreNoRecords=ignoreNoRecords)
394
395
396     def cancel(self, ignoreNoRecords=None):
397         """Cancel all changes to the current record and all children.
398
399         Two hook methods will be called: beforeCancel() and afterCancel(). The
400         former, if it returns an error message, will raise an exception and not
401         continue cancelling the record.
402         """
403         errMsg = self.beforeCancel()
404         if errMsg:
405             raise dException.BusinessRuleViolation, errMsg
406         if ignoreNoRecords is None:
407             # Canceling changes when there are no records should
408             # normally not be a problem.
409             ignoreNoRecords = True
410         # Tell the cursor and all children to cancel themselves:
411         self._CurrentCursor.cancel(ignoreNoRecords=ignoreNoRecords)
412         for child in self.__children:
413             child.cancelAll(ignoreNoRecords=ignoreNoRecords)
414         self.afterCancel()
415
416
417     def deleteAllChildren(self, startTransaction=True):
418         """Delete all children associated with the current record without
419         deleting the current record in this bizobj.
420         """
421         cursor = self._CurrentCursor
422         app = self.Application
423         errMsg = self.beforeDeleteAllChildren()
424         if errMsg:
425             raise dException.BusinessRuleViolation, errMsg
426
427         isTransactionManager = False
428         if startTransaction:
429             isTransactionManager = app.getTransactionToken(self)
430             if isTransactionManager:
431                 cursor.beginTransaction()
432
433         try:
434             for child in self.__children:
435                 child.deleteAll(startTransaction=False)
436             if isTransactionManager:
437                 cursor.commitTransaction()
438                 app.releaseTransactionToken(self)
439
440         except dException.DBQueryException, e:
441             if isTransactionManager:
442                 cursor.rollbackTransaction()
443                 app.releaseTransactionToken(self)
444             raise dException.DBQueryException, e
445         except StandardError, e:
446             if isTransactionManager:
447                 cursor.rollbackTransaction()
448                 app.releaseTransactionToken(self)
449             raise StandardError, e
450         self.afterDeleteAllChildren()
451
452
453     def delete(self, startTransaction=True, inLoop=False):
454         """Delete the current row of the data set."""
455         app = self.Application
456         cursor = self._CurrentCursor
457         errMsg = self.beforeDelete()
458         if not errMsg:
459             errMsg = self.beforePointerMove()
460         if errMsg:
461             raise dException.BusinessRuleViolation, errMsg
462
463         if self.KeyField is None:
464             raise dException.dException, _("No key field defined for table: ") + self.DataSource
465
466         if self.deleteChildLogic == kons.REFINTEG_RESTRICT:
467             # See if there are any child records
468             for child in self.__children:
469                 if child.RowCount > 0:
470                     raise dException.dException, _("Deletion prohibited - there are related child records.")
471
472         isTransactionManager = False
473         if startTransaction:
474             isTransactionManager = app.getTransactionToken(self)
475             if isTransactionManager:
476                 cursor.beginTransaction()
477
478         try:
479             cursor.delete()
480             if self.RowCount == 0:
481                 # Hook method for handling the deletion of the last record in the cursor.
482                 self.onDeleteLastRecord()
483             # Now cycle through any child bizobjs and fire their cancel() methods. This will
484             # ensure that any changed data they may have is reverted. They are then requeried to
485             # populate them with data for the current record in this bizobj.
486             for child in self.__children:
487                 if self.deleteChildLogic == kons.REFINTEG_CASCADE:
488                     child.deleteAll(startTransaction=False)
489                 else:
490                     child.cancelAll()
491                     child.requery()
492
493             if isTransactionManager:
494                 cursor.commitTransaction()
495                 app.releaseTransactionToken(self)
496
497             if not inLoop:
498                 self.afterPointerMove()
499                 self.afterChange()
500                 self.afterDelete()
501         except dException.DBQueryException, e:
502             if isTransactionManager:
503                 cursor.rollbackTransaction()
504                 app.releaseTransactionToken(self)
505             raise dException.DBQueryException, e
506         except StandardError, e:
507             if isTransactionManager:
508                 cursor.rollbackTransaction()
509                 app.releaseTransactionToken(self)
510             raise StandardError, e
511
512
513     def deleteAll(self, startTransaction=True):
514         """ Delete all rows in the data set."""
515         isTransactionManager = False
516         if startTransaction:
517             isTransactionManager = app.getTransactionToken(self)
518             if isTransactionManager:
519                 cursor.beginTransaction()
520         try:
521             while self.RowCount > 0:
522                 self.first()
523                 ret = self.delete(startTransaction=False, inLoop=True)
524             if isTransactionManager:
525                 cursor.commitTransaction()
526                 app.releaseTransactionToken(self)
527
528             self.afterPointerMove()
529             self.afterChange()
530             self.afterDelete()
531         except dException.DBQueryException, e:
532             if isTransactionManager:
533                 cursor.rollbackTransaction()
534                 app.releaseTransactionToken(self)
535             raise dException.DBQueryException, e
536         except StandardError, e:
537             if isTransactionManager:
538                 cursor.rollbackTransaction()
539                 app.releaseTransactionToken(self)
540             raise StandardError, e
541
542
543     def execute(self, sql):
544         """Execute the sql on the cursor. Dangerous. Use executeSafe instead."""
545         self._syncWithCursors()
546         return self._CurrentCursor.execute(sql)
547
548
549     def executeSafe(self, sql):
550         """Execute the passed SQL using an auxiliary cursor.
551
552         This is considered 'safe', because it won't harm the contents of the
553         main cursor.
554         """
555         self._syncWithCursors()
556         return self._CurrentCursor.executeSafe(sql)
557
558
559     def getChangedRows(self, includeNewUnchanged=False):
560         """ Returns a list of row numbers for which isChanged() returns True. The
561         changes may therefore not be in the record itself, but in a dependent child
562         record. If includeNewUnchanged is True, the presence of a new unsaved
563         record that has not been modified from its defaults will suffice to mark the
564         record as changed.
565         """
566         if self.__children:
567             # Must iterate all records to find potential changes in children:
568             self.__changedRows = []
569             self.scan(self._listChangedRows)
570             return self.__changedRows
571         else:
572             # Can use the much faster cursor.getChangedRows():
573             return self._CurrentCursor.getChangedRows(includeNewUnchanged)
574
575
576     def _listChangedRows(self):
577         """ Called from a scan loop. If the current record is changed,
578         append the RowNumber to the list.
579         """
580         if self.isChanged():
581             self.__changedRows.append(self.RowNumber)
582
583
584     def getRecordStatus(self, rownum=None):
585         """ Returns a dictionary containing an element for each changed
586         field in the specified record (or the current record if none is specified).
587         The field name is the key for each element; the value is a 2-element
588         tuple, with the first element being the original value, and the second
589         being the current value.
590         """
591         if rownum is None:
592             rownum = self.RowNumber
593         return self._CurrentCursor.getRecordStatus(rownum)
594
595
596     def bizIterator(self):
597         """Returns an iterator that moves the bizobj's record pointer from
598         the first record to the last. You may call the iterator's reverse() method
599         before beginning iteration in order to iterate from the last record
600         back to the first.
601         """
602         return _bizIterator(self)
603
604
605     def scan(self, func, *args, **kwargs):
606         """Iterate over all records and apply the passed function to each.
607
608         Set self.exitScan to True to exit the scan on the next iteration.
609
610         If self.ScanRestorePosition is True, the position of the current
611         record in the recordset is restored after the iteration. If
612         self.ScanReverse is True, the records are processed in reverse order.
613         """
614         self.scanRows(func, range(self.RowCount), *args, **kwargs)
615
616
617     def scanRows(self, func, rows, *args, **kwargs):
618         """Iterate over the specified rows and apply the passed function to each.
619
620         Set self.exitScan to True to exit the scan on the next iteration.
621         """
622         # Flag that the function can set to prematurely exit the scan
623         self.exitScan = False
624         rows = list(rows)
625         try:
626             currPK = self.getPK()
627             currRow = None
628         except: