Package camelot :: Package camelot :: Package view :: Package proxy :: Module collection_proxy
[hide private]
[frames] | no frames]

Source Code for Module camelot.camelot.view.proxy.collection_proxy

  1  #  ============================================================================
 
  2  #
 
  3  #  Copyright (C) 2007-2008 Conceptive Engineering bvba. All rights reserved.
 
  4  #  www.conceptive.be / project-camelot@conceptive.be
 
  5  #
 
  6  #  This file is part of the Camelot Library.
 
  7  #
 
  8  #  This file may be used under the terms of the GNU General Public
 
  9  #  License version 2.0 as published by the Free Software Foundation
 
 10  #  and appearing in the file LICENSE.GPL included in the packaging of
 
 11  #  this file.  Please review the following information to ensure GNU
 
 12  #  General Public Licensing requirements will be met:
 
 13  #  http://www.trolltech.com/products/qt/opensource.html
 
 14  #
 
 15  #  If you are unsure which license is appropriate for your use, please
 
 16  #  review the following information:
 
 17  #  http://www.trolltech.com/products/qt/licensing.html or contact
 
 18  #  project-camelot@conceptive.be.
 
 19  #
 
 20  #  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 
 21  #  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 
 22  #
 
 23  #  For use of this library in commercial applications, please contact
 
 24  #  project-camelot@conceptive.be
 
 25  #
 
 26  #  ============================================================================
 
 27  
 
 28  """Proxy representing a collection of entities that live in the model thread.
 
 29  
 
 30  The proxy represents them in the gui thread and provides access to the data
 
 31  with zero delay.  If the data is not yet present in the proxy, dummy data is
 
 32  returned and an update signal is emitted when the correct data is available.
 
 33  """ 
 34  
 
 35  import logging 
 36  logger = logging.getLogger('camelot.view.proxy.collection_proxy') 
 37  verbose = False  
 38  
 
 39  import pickle 
 40  import elixir 
 41  import datetime 
 42  from PyQt4 import QtGui 
 43  from PyQt4 import QtCore 
 44  from PyQt4.QtCore import Qt 
 45  
 
 46  from sqlalchemy.orm.session import Session 
 47  from camelot.view import art 
 48  from camelot.view.fifo import fifo 
 49  from camelot.view.controls import delegates 
 50  from camelot.view.remote_signals import get_signal_handler 
 51  from camelot.view.model_thread import gui_function 
 52  from camelot.view.model_thread import model_function 
 53  from camelot.view.model_thread import get_model_thread 
54 55 56 -class DelayedProxy(object):
57 """A proxy object needs to be constructed within the GUI thread. Construct 58 a delayed proxy when the construction of a proxy is needed within the Model 59 thread. On first occasion the delayed proxy will be converted to a real 60 proxy within the GUI thread 61 """ 62 63 @model_function
64 - def __init__(self, *args, **kwargs):
65 self.args = args 66 self.kwargs = kwargs
67 68 @gui_function
69 - def __call__(self):
70 return CollectionProxy(*self.args, **self.kwargs)
71
72 @model_function 73 -def RowDataFromObject(obj, columns):
74 """Create row data from an object, by fetching its attributes""" 75 row_data = [] 76 mt = get_model_thread() 77 78 def create_collection_getter(o, attr): 79 return lambda: getattr(o, attr)
80 81 for i,col in enumerate(columns): 82 field_attributes = col[1] 83 if field_attributes['python_type'] == list: 84 row_data.append(DelayedProxy(field_attributes['admin'], 85 create_collection_getter(obj, col[0]), 86 field_attributes['admin'].getColumns)) 87 else: 88 row_data.append(getattr(obj, col[0])) 89 return row_data 90
91 -def RowDataAsUnicode(row_data):
92 def unicode_or_none(data): 93 if data: 94 if isinstance(data, list): 95 return '.'.join(data) 96 elif isinstance(data, datetime.datetime): 97 # datetime should come before date since datetime is a subtype of date 98 if data.year >= 1900: 99 return data.strftime('%d/%m/%Y %H:%M') 100 elif isinstance(data, datetime.date): 101 if data.year >= 1900: 102 return data.strftime('%d/%m/%Y') 103 return unicode(data) 104 return data
105 106 return [unicode_or_none(col) for col in row_data] 107
108 109 -class EmptyRowData(object):
110 - def __getitem__(self, column):
111 return None
112 113 empty_row_data = EmptyRowData()
114 115 -class CollectionProxy(QtCore.QAbstractTableModel):
116 """The CollectionProxy contains a limited copy of the data in the actual 117 collection, usable for fast visualisation in a QTableView 118 """ 119 120 @gui_function
121 - def __init__(self, admin, collection_getter, columns_getter, 122 max_number_of_rows=10, edits=None, flush_changes=True):
123 """@param admin: the admin interface for the items in the collection 124 125 @param collection_getter: a function that takes no arguments and returns 126 the collection that will be visualized. This function will be called inside 127 the model thread, to prevent delays when this function causes the database 128 to be hit. 129 130 @param columns_getter: a function that takes no arguments and returns the 131 columns that will be cached in the proxy. This function will be called 132 inside the model thread. 133 """ 134 logger.debug('initialize query table for %s' % (admin.getName())) 135 self.logger = logger 136 QtCore.QAbstractTableModel.__init__(self) 137 self.admin = admin 138 self.form_icon = QtCore.QVariant(QtGui.QIcon(art.icon16('places/folder'))) 139 self.validator = admin.createValidator(self) 140 self.collection_getter = collection_getter 141 self.column_count = 0 142 self.flush_changes = flush_changes 143 self.mt = admin.getModelThread() 144 # Set database connection and load data 145 self.rows = 0 146 self._columns = [] 147 self.max_number_of_rows = max_number_of_rows 148 self.cache = {Qt.DisplayRole:fifo(10*self.max_number_of_rows), 149 Qt.EditRole:fifo(10*self.max_number_of_rows)} 150 # The rows in the table for which a cache refill is under request 151 self.rows_under_request = set() 152 # The rows that have unflushed changes 153 self.unflushed_rows = set() 154 # Set edits 155 self.edits = edits or [] 156 self.rsh = get_signal_handler() 157 self.rsh.connect(self.rsh, 158 self.rsh.entity_update_signal, 159 self.handleEntityUpdate) 160 self.rsh.connect(self.rsh, 161 self.rsh.entity_delete_signal, 162 self.handleEntityDelete) 163 self.rsh.connect(self.rsh, 164 self.rsh.entity_create_signal, 165 self.handleEntityCreate) 166 167 def get_columns(): 168 self._columns = columns_getter() 169 return self._columns
170 171 self.mt.post(get_columns, lambda columns:self.setColumns(columns)) 172 # in that way the number of rows is requested as well 173 self.mt.post(self.getRowCount, self.setRowCount) 174 logger.debug('initialization finished') 175 self.item_delegate = None
176
177 - def hasUnflushedRows(self):
178 """The model has rows that have not been flushed to the database yet, 179 because the row is invalid 180 """ 181 return len(self.unflushed_rows) > 0
182 183 @model_function
184 - def getRowCount(self):
185 return len(self.collection_getter())
186 187 @gui_function
188 - def revertRow(self, row):
189 def create_refresh_entity(row): 190 @model_function 191 def refresh_entity(): 192 o = self._get_object(row) 193 elixir.session.refresh(o) 194 self.rsh.sendEntityUpdate(self, o) 195 return row, o
196 197 return refresh_entity 198 199 def refresh(row_and_entity): 200 row, entity = row_and_entity 201 self.handleRowUpdate(row) 202 self.rsh.sendEntityUpdate(self, entity) 203 204 self.mt.post(create_refresh_entity(row), refresh) 205
206 - def refresh(self):
207 def refresh_content(rows): 208 self.cache = {Qt.DisplayRole: fifo(10*self.max_number_of_rows), 209 Qt.EditRole: fifo(10*self.max_number_of_rows)} 210 self.setRowCount(rows)
211 212 self.mt.post(self.getRowCount, refresh_content) 213
214 - def setCollectionGetter(self, collection_getter):
215 self.collection_getter = collection_getter 216 self.refresh()
217
218 - def handleRowUpdate(self, row):
219 """Handles the update of a row when this row might be out of date""" 220 self.cache[Qt.DisplayRole].delete_by_row(row) 221 self.cache[Qt.EditRole].delete_by_row(row) 222 sig = 'dataChanged(const QModelIndex &, const QModelIndex &)' 223 self.emit(QtCore.SIGNAL(sig), 224 self.index(row, 0), 225 self.index(row, self.column_count))
226
227 - def handleEntityUpdate(self, sender, entity):
228 """Handles the entity signal, indicating that the model is out of date""" 229 logger.debug('%s %s received entity update signal' % \ 230 (self.__class__.__name__, self.admin.getName())) 231 if sender != self: 232 row = self.cache[Qt.DisplayRole].delete_by_entity(entity) 233 row = self.cache[Qt.EditRole].delete_by_entity(entity) 234 if row: 235 logger.debug('updated row %i' % row) 236 sig = 'dataChanged(const QModelIndex &, const QModelIndex &)' 237 self.emit(QtCore.SIGNAL(sig), 238 self.index(row, 0), 239 self.index(row, self.column_count)) 240 else: 241 logger.debug('entity not in cache') 242 else: 243 logger.debug('duplicate update')
244
245 - def handleEntityDelete(self, sender, entity, primary_keys):
246 """Handles the entity signal, indicating that the model is out of date""" 247 logger.debug('received entity delete signal') 248 if sender != self: 249 self.refresh()
250
251 - def handleEntityCreate(self, entity, primary_keys):
252 """Handles the entity signal, indicating that the model is out of date""" 253 logger.debug('received entity create signal') 254 if sender != self: 255 self.refresh()
256
257 - def setRowCount(self, rows):
258 """Callback method to set the number of rows 259 @param rows the new number of rows 260 """ 261 self.rows = rows 262 self.emit(QtCore.SIGNAL('layoutChanged()'))
263
264 - def getItemDelegate(self):
265 logger.debug('getItemDelegate') 266 if not self.item_delegate: 267 raise Exception('item delegate not yet available') 268 return self.item_delegate
269
270 - def getColumns(self):
271 return self._columns
272
273 - def setColumns(self, columns):
274 """Callback method to set the columns 275 276 @param columns a list with fields to be displayed 277 """ 278 279 self.column_count = len(columns) 280 self._columns = columns 281 282 self.item_delegate = delegates.GenericDelegate() 283 self.item_delegate.set_columns_desc(columns) 284 285 for i, c in enumerate(columns): 286 287 field_name = c[0] 288 type_ = c[1]['python_type'] 289 widget_ = c[1]['widget'] 290 291 if verbose: 292 logger.debug("creating delegate for %s \ntype: %s\nwidget: %s\n" \ 293 "arguments: %s" % (field_name, type_, widget_, str(c[1]))) 294 else: 295 logger.debug('creating delegate for %s' % field_name) 296 297 298 if 'delegate' in c[1]: 299 delegate = c[1]['delegate'](parent=None, **c[1]) 300 self.item_delegate.insertColumnDelegate(i, delegate) 301 continue 302 if 'choices' in c[1]: 303 delegate = delegates.ComboBoxColumnDelegate(**c[1]) 304 self.item_delegate.insertColumnDelegate(i, delegate) 305 continue 306 if widget_ == 'code': 307 delegate = delegates.CodeColumnDelegate(c[1]['parts']) 308 self.item_delegate.insertColumnDelegate(i, delegate) 309 continue 310 elif widget_ == 'datetime': 311 delegate = delegates.DateTimeColumnDelegate(parent=None, **c[1]) 312 self.item_delegate.insertColumnDelegate(i, delegate) 313 elif widget_ == 'virtual_address': 314 delegate = delegates.VirtualAddressColumnDelegate() 315 self.item_delegate.insertColumnDelegate(i, delegate) 316 continue 317 elif widget_ == 'image': 318 delegate = delegates. ImageColumnDelegate() 319 self.item_delegate.insertColumnDelegate(i, delegate) 320 continue 321 elif widget_ == 'many2one': 322 entity_admin = c[1]['admin'] 323 delegate = delegates.Many2OneColumnDelegate(**c[1]) 324 self.item_delegate.insertColumnDelegate(i, delegate) 325 elif widget_ == 'one2many': 326 delegate = delegates.One2ManyColumnDelegate(**c[1]) 327 self.item_delegate.insertColumnDelegate(i, delegate) 328 elif type_ == str: 329 if c[1]['length']: 330 delegate = delegates.PlainTextColumnDelegate(maxlength=c[1]['length']) 331 self.item_delegate.insertColumnDelegate(i, delegate) 332 else: 333 delegate = delegates.RichTextColumnDelegate(**c[1]) 334 self.item_delegate.insertColumnDelegate(i, delegate) 335 elif type_ == int: 336 delegate = delegates.IntegerColumnDelegate(0, 100000) 337 self.item_delegate.insertColumnDelegate(i, delegate) 338 elif type_ == datetime.date: 339 delegate = delegates.DateColumnDelegate(format='dd/MM/yyyy', 340 default=c[1].get('default', None), 341 nullable=c[1].get('nullable', False)) 342 self.item_delegate.insertColumnDelegate(i, delegate) 343 elif widget_ == 'time': 344 delegate = delegates.TimeColumnDelegate(format=c[1].get('format'), 345 default=c[1].get('default', None), 346 nullable=c[1].get('nullable', False)) 347 self.item_delegate.insertColumnDelegate(i, delegate) 348 elif type_ == float: 349 delegate = delegates.FloatColumnDelegate(-100000.0, 100000.0, **c[1]) 350 self.item_delegate.insertColumnDelegate(i, delegate) 351 elif type_ == bool: 352 delegate = delegates.BoolColumnDelegate() 353 self.item_delegate.insertColumnDelegate(i, delegate) 354 else: 355 delegate = delegates.PlainTextColumnDelegate() 356 self.item_delegate.insertColumnDelegate(i, delegate) 357 self.emit(QtCore.SIGNAL('layoutChanged()'))
358
359 - def rowCount(self, index=None):
360 return self.rows
361
362 - def columnCount(self, index=None):
363 return self.column_count
364 365 @gui_function
366 - def headerData(self, section, orientation, role):
367 """In case the columns have not been set yet, don't even try to get 368 information out of them 369 """ 370 if (orientation == Qt.Horizontal) and (section >= self.column_count): 371 return QtCore.QAbstractTableModel.headerData(self, section, 372 orientation, role) 373 if role == Qt.DisplayRole: 374 if orientation == Qt.Horizontal: 375 return QtCore.QVariant(self._columns[section][1]['name']) 376 elif orientation == Qt.Vertical: 377 #return QtCore.QVariant(int(section+1)) 378 # we don't want anything to be displayed 379 return QtCore.QVariant() 380 if role == Qt.FontRole: 381 if orientation == Qt.Horizontal: 382 font = QtGui.QApplication.font() 383 if ('nullable' in self._columns[section][1]) and \ 384 (self._columns[section][1]['nullable']==False): 385 font.setBold(True) 386 return QtCore.QVariant(font) 387 else: 388 font.setBold(False) 389 return QtCore.QVariant(font) 390 #if role == Qt.SizeHintRole: 391 # label = QtGui.QLabel(self._columns[section][1]['name']) 392 # return QtCore.QVariant(label.sizeHint()) 393 if role == Qt.DecorationRole: 394 if orientation == Qt.Vertical: 395 return self.form_icon 396 return QtCore.QAbstractTableModel.headerData(self, section, orientation, role)
397 398 @gui_function
399 - def data(self, index, role):
400 import datetime 401 if not index.isValid() or \ 402 not (0 <= index.row() <= self.rowCount(index)) or \ 403 not (0 <= index.column() <= self.columnCount(index)): 404 return QtCore.QVariant() 405 if role in (Qt.DisplayRole, Qt.EditRole): 406 data = self._get_row_data(index.row(), role) 407 try: 408 value = data[index.column()] 409 if isinstance(value, DelayedProxy): 410 value = value() 411 data[index.column()] = value 412 except KeyError: 413 logger.error('Programming error, could not find data of column %s in %s'%(index.column(), str(data))) 414 value = None 415 return QtCore.QVariant(value) 416 elif role == Qt.SizeHintRole: 417 c = self.getColumns()[index.column()] 418 type_ = c[1]['python_type'] 419 widget_ = c[1]['widget'] 420 if type_ == datetime.date: 421 from camelot.view.controls.editors import DateEditor 422 editor = DateEditor() 423 return QtCore.QVariant(editor.sizeHint()) 424 elif widget_ == 'one2many': 425 from camelot.view.controls.editors import One2ManyEditor 426 entity_name = c[0] 427 entity_admin = c[1]['admin'] 428 editor = One2ManyEditor(entity_admin, entity_name) 429 sh = editor.sizeHint() 430 return QtCore.QVariant(sh) 431 elif widget_ == 'many2one': 432 from camelot.view.controls.editors import Many2OneEditor 433 entity_admin = c[1]['admin'] 434 editor = Many2OneEditor(entity_admin) 435 sh = editor.sizeHint() 436 return QtCore.QVariant(sh) 437 elif role == Qt.ForegroundRole: 438 pass 439 elif role == Qt.BackgroundRole: 440 pass 441 return QtCore.QVariant()
442
443 - def setData(self, index, value, role=Qt.EditRole):
444 """Value should be a function taking no arguments that returns the data to 445 be set 446 447 This function will then be called in the model_thread 448 """ 449 if role == Qt.EditRole: 450 451 flushed = (index.row() not in self.unflushed_rows) 452 self.unflushed_rows.add(index.row()) 453 454 def make_update_function(row, column, value): 455 456 @model_function 457 def update_model_and_cache(): 458 new_value = value() 459 if verbose: 460 logger.debug('set data for col %s;row %s to %s' % (row, column, new_value)) 461 else: 462 logger.debug('set data for col %s;row %s' % (row, column)) 463 464 o = self._get_object(row) 465 if not o: 466 # the object might have been deleted from the collection while the editor 467 # was still open 468 try: 469 self.unflushed_rows.remove(row) 470 except KeyError: 471 pass 472 return 473 attribute, field_attributes = self.getColumns()[column] 474 old_value = getattr(o, attribute) 475 if new_value!=old_value and field_attributes['editable']==True: 476 # update the model 477 model_updated = False 478 try: 479 setattr(o, attribute, new_value) 480 model_updated = True 481 except AttributeError: 482 logger.error("Can't set attribute %s to %s"%(attribute, str(value))) 483 except TypeError: 484 # type error can be raised in case we try to set to a collection 485 pass 486 # update the cache 487 row_data = RowDataFromObject(o, self.getColumns()) 488 self.cache[Qt.EditRole].add_data(row, o, row_data) 489 self.cache[Qt.DisplayRole].add_data(row, o, RowDataAsUnicode(row_data)) 490 if self.flush_changes and self.validator.isValid(row): 491 # save the state before the update 492 elixir.session.flush([o]) 493 try: 494 self.unflushed_rows.remove(row) 495 except KeyError: 496 pass 497 if model_updated: 498 # 499 # in case of images, we cannot pickle them 500 # 501 if not 'Imag' in old_value.__class__.__name__: 502 from camelot.model.memento import BeforeUpdate 503 from camelot.model.authentication import getCurrentPerson 504 history = BeforeUpdate(model=unicode(self.admin.entity.__name__), 505 primary_key=o.id, 506 previous_attributes={attribute:old_value}, 507 person = getCurrentPerson()) 508 elixir.session.flush([history]) 509 #@todo: update should only be sent remotely when flush was done 510 self.rsh.sendEntityUpdate(self, o) 511 return ((row,0), (row,len(self.getColumns()))) 512 elif flushed: 513 try: 514 self.unflushed_rows.remove(row) 515 except KeyError: 516 pass
517 518 return update_model_and_cache 519 520 def emit_changes(region): 521 if region: 522 self.emit(QtCore.SIGNAL('dataChanged(const QModelIndex &, const QModelIndex &)'), 523 self.index(region[0][0],region[0][1]), self.index(region[1][0],region[1][1])) 524 525 self.mt.post(make_update_function(index.row(), index.column(), value), emit_changes) 526 527 return True 528
529 - def flags(self, index):
530 flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable 531 if self.getColumns()[index.column()][1]['editable']: 532 flags = flags | Qt.ItemIsEditable 533 return flags
534 535 @model_function
536 - def _extend_cache(self, offset, limit):
537 """Extend the cache around row""" 538 #@TODO : also store the primary key, here we just saved the id 539 columns = self.getColumns() 540 offset = min(offset, self.rows) 541 limit = min(limit, self.rows-offset) 542 for i,o in enumerate(self.collection_getter()[offset:offset+limit+1]): 543 row_data = RowDataFromObject(o, columns) 544 self.cache[Qt.EditRole].add_data(i+offset, o, row_data) 545 self.cache[Qt.DisplayRole].add_data(i+offset, o, RowDataAsUnicode(row_data)) 546 return (offset, limit)
547 548 @model_function
549 - def _get_object(self, row):
550 """Get the object corresponding to row""" 551 try: 552 # first try to get the primary key out of the cache, if it's not 553 # there, query the collection_getter 554 return self.cache[Qt.EditRole].get_entity_at_row(row) 555 except KeyError: 556 pass 557 return self.collection_getter()[row]
558
559 - def _cache_extended(self, offset, limit):
560 self.rows_under_request.difference_update(set(range(offset, offset+limit))) 561 self.emit(QtCore.SIGNAL('dataChanged(const QModelIndex &, const QModelIndex &)'), 562 self.index(offset,0), self.index(offset+limit,self.column_count))
563
564 - def _get_row_data(self, row, role):
565 """Get the data which is to be visualized at a certain row of the 566 table, if needed, post a refill request the cache to get the object 567 and its neighbours in the cache, meanwhile, return an empty object 568 @param role: Qt.EditRole or Qt.DisplayRole 569 """ 570 role_cache = self.cache[role] 571 try: 572 return role_cache.get_data_at_row(row) 573 except KeyError: 574 if row not in self.rows_under_request: 575 offset = max(row-self.max_number_of_rows/2,0) 576 limit = self.max_number_of_rows 577 self.rows_under_request.update(set(range(offset, offset+limit))) 578 self.mt.post(lambda :self._extend_cache(offset, limit), 579 lambda interval:self._cache_extended(*interval)) 580 return empty_row_data
581 582 @model_function
583 - def remove(self, o):
584 self.collection_getter().remove(o) 585 self.rows -= 1
586 587 @model_function
588 - def append(self, o):
589 self.collection_getter().append(o) 590 self.rows += 1
591 592 @model_function
593 - def removeEntityInstance(self, o):
594 logger.debug('remove entity instance with id %s' % o.id) 595 self.remove(o) 596 # remove the entity from the cache 597 self.cache[Qt.DisplayRole].delete_by_entity(o) 598 self.cache[Qt.EditRole].delete_by_entity(o) 599 self.rsh.sendEntityDelete(self, o) 600 if o.id: 601 pk = o.id 602 # save the state before the update 603 from camelot.model.memento import BeforeDelete 604 from camelot.model.authentication import getCurrentPerson 605 history = BeforeDelete(model=unicode(self.admin.entity.__name__), 606 primary_key=pk, 607 previous_attributes={}, 608 person = getCurrentPerson()) 609 logger.debug('delete the object') 610 o.delete() 611 Session.object_session(o).flush([o]) 612 Session.object_session(history).flush([history]) 613 self.mt.post(lambda:None, lambda *args:self.refresh())
614 615 @gui_function
616 - def removeRow(self, row):
617 logger.debug('remove row %s' % row) 618 619 def create_delete_function(row): 620 621 def delete_function(): 622 o = self._get_object(row) 623 self.removeEntityInstance(o)
624 625 return delete_function 626 627 self.mt.post(create_delete_function(row)) 628 return True 629 630 @model_function
631 - def insertEntityInstance(self, row, o):
632 self.append(o) 633 row = self.getRowCount()-1 634 self.unflushed_rows.add(row) 635 if self.flush_changes and not len(self.validator.objectValidity(o)): 636 elixir.session.flush([o]) 637 try: 638 self.unflushed_rows.remove(row) 639 except KeyError: 640 pass 641 from camelot.model.memento import Create 642 from camelot.model.authentication import getCurrentPerson 643 history = Create(model=unicode(self.admin.entity.__name__), 644 primary_key=o.id, 645 person = getCurrentPerson()) 646 elixir.session.flush([history]) 647 self.rsh.sendEntityCreate(self, o) 648 self.mt.post(lambda:None, lambda *args:self.refresh())
649 650 @gui_function
651 - def insertRow(self, row, entity_instance_getter):
652 653 def create_insert_function(getter): 654 655 @model_function 656 def insert_function(): 657 self.insertEntityInstance(row, getter())
658 659 return insert_function 660 661 self.mt.post(create_insert_function(entity_instance_getter)) 662
663 - def __del__(self):
664 logger.warn('delete CollectionProxy')
665