camelot.camelot.view.proxy.collection_proxy

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