Package Camelot :: Package camelot :: Package view :: Package proxy :: Module collection_proxy |
[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

     38  import elixir
     39  import datetime
     40  from PyQt4.QtCore import Qt
     41  from PyQt4 import QtGui, QtCore
     42  import sip
     43

     44  from camelot.view.art import Icon
     45  from camelot.view.fifo import fifo
     46  from camelot.view.controls import delegates
     47  from camelot.view.remote_signals import get_signal_handler
     48  from camelot.view.model_thread import gui_function, \

     49                                        model_function, post
     50

     51

     52 -class DelayedProxy( object ):
     53      """A proxy object needs to be constructed within the GUI thread. Construct

     54      a delayed proxy when the construction of a proxy is needed within the Model

     55      thread.  On first occasion the delayed proxy will be converted to a real

     56      proxy within the GUI thread

     57      """
     58

     59      @model_function
     60 -    def __init__( self, *args, **kwargs ):
     61          self.args = args
     62          self.kwargs = kwargs
     63

     64      @gui_function
     65 -    def __call__( self ):
     66          return CollectionProxy( *self.args, **self.kwargs )
     67

     68  @model_function
     69 -def tool_tips_from_object(obj, columns):
     70

     71      data = []
     72

     73      for col in columns:
     74          tooltip_getter = col[1]['tooltip']
     75          if tooltip_getter:
     76              try:
     77                  data.append( tooltip_getter(obj) )
     78              except Exception, e:
     79                  logger.error('Programming Error : error in tooltip function', exc_info=e)
     80                  data.append(None)
     81          else:
     82              data.append( None )
     83

     84      return data
     85

     86  @model_function
     87 -def background_colors_from_object(obj, columns):
     88

     89      data = []
     90

     91      for col in columns:
     92          background_color_getter = col[1]['background_color']
     93          if background_color_getter:
     94              try:
     95                  data.append( background_color_getter(obj) )
     96              except Exception, e:
     97                  logger.error('Programming Error : error in background_color function', exc_info=e)
     98                  data.append(None)
     99          else:
    100              data.append( None )
    101

    102      return data
    103

    104  @model_function
    105 -def strip_data_from_object( obj, columns ):
    106      """For every column in columns, get the corresponding value from the

    107      object.  Getting a value from an object is time consuming, so using

    108      this function should be minimized.

    109      :param obj: the object of which to get data

    110      :param columns: a list of columns for which to get data

    111      """
    112      row_data = []
    113

    114      def create_collection_getter( o, attr ):
    115          return lambda: getattr( o, attr )
    116

    117      for _i, col in enumerate( columns ):
    118          field_attributes = col[1]
    119          try:
    120              getter = field_attributes['getter']
    121              if field_attributes['python_type'] == list:
    122                  row_data.append( DelayedProxy( field_attributes['admin'],

    123                                  create_collection_getter( obj, col[0] ),

    124                                  field_attributes['admin'].get_columns ) )
    125              else:
    126                  row_data.append( getter( obj ) )
    127          except Exception, e:
    128              logger.error('ProgrammingError : could not get attribute %s of object of type %s'%(col[0], obj.__class__.__name__),

    129                           exc_info=e)
    130              row_data.append( None )
    131      return row_data
    132

    133  @model_function
    134 -def stripped_data_to_unicode( stripped_data, obj, columns ):
    135      """Extract for each field in the row data a 'visible' form of

    136      data"""
    137

    138      row_data = []
    139

    140      for field_data, ( _field_name, field_attributes ) in zip( stripped_data, columns ):
    141          unicode_data = u''
    142          if 'unicode_format' in field_attributes:
    143              unicode_format = field_attributes['unicode_format']
    144              if field_data != None:
    145                  unicode_data = unicode_format( field_data )
    146          elif 'choices' in field_attributes:
    147              choices = field_attributes['choices']
    148              if callable(choices):
    149                  try:
    150                      for key, value in choices( obj ):
    151                          if key == field_data:
    152                              unicode_data = value
    153                              continue
    154                  except Exception, e:
    155                      logger.error('Programming Error : could not evaluate choices function', exc_info=e)
    156              else:
    157                  unicode_data = field_data
    158          elif isinstance( field_data, DelayedProxy ):
    159              unicode_data = u'...'
    160          elif isinstance( field_data, list ):
    161              unicode_data = u'.'.join( [unicode( e ) for e in field_data] )
    162          elif isinstance( field_data, datetime.datetime ):
    163              # datetime should come before date since datetime is a subtype of date

    164              if field_data.year >= 1900:
    165                  unicode_data = field_data.strftime( '%d/%m/%Y %H:%M' )
    166          elif isinstance( field_data, datetime.date ):
    167              if field_data.year >= 1900:
    168                  unicode_data = field_data.strftime( '%d/%m/%Y' )
    169          elif field_data != None:
    170              unicode_data = unicode( field_data )
    171          row_data.append( unicode_data )
    172

    173      return row_data
    174

    175  from camelot.view.proxy import ValueLoading
    176

    177 -class EmptyRowData( object ):
    178 -    def __getitem__( self, column ):
    179          return ValueLoading
    180          return None
    181

    182  empty_row_data = EmptyRowData()
    183

    184 -class SortingRowMapper( dict ):
    185      """Class mapping rows of a collection 1:1 without sorting

    186      and filtering, unless a mapping has been defined explicitly"""
    187

    188 -    def __getitem__(self, row):
    189          try:
    190              return super(SortingRowMapper, self).__getitem__(row)
    191          except KeyError:
    192              return row
    193

    194 -class CollectionProxy( QtCore.QAbstractTableModel ):
    195      """The CollectionProxy contains a limited copy of the data in the actual

    196      collection, usable for fast visualisation in a QTableView

    197

    198      the CollectionProxy has some class attributes that can be overwritten when

    199      subclassing it :

    200

    201      * header_icon : the icon to be used in the vertical header

    202

    203      """
    204

    205      _header_font = QtGui.QApplication.font()
    206      _header_font_required = QtGui.QApplication.font()
    207      _header_font_required.setBold( True )
    208

    209      header_icon = Icon( 'tango/16x16/places/folder.png' )
    210

    211      item_delegate_changed_signal = QtCore.SIGNAL('itemDelegateChanged')
    212      rows_removed_signal = QtCore.SIGNAL('rowsRemoved(const QModelIndex&,int,int)')
    213

    214      @gui_function
    215 -    def __init__( self, admin, collection_getter, columns_getter,

    216                   max_number_of_rows = 10, edits = None, flush_changes = True ):
    217          """@param admin: the admin interface for the items in the collection

    218

    219          @param collection_getter: a function that takes no arguments and returns

    220          the collection that will be visualized. This function will be called inside

    221          the model thread, to prevent delays when this function causes the database

    222          to be hit.  If the collection is a list, it should not contain any duplicate

    223          elements.

    224

    225          @param columns_getter: a function that takes no arguments and returns the

    226          columns that will be cached in the proxy. This function will be called

    227          inside the model thread.

    228          """
    229          from camelot.view.model_thread import get_model_thread
    230          self.logger = logging.getLogger(logger.name + '.%s'%id(self))
    231          self.logger.debug('initialize query table for %s' % (admin.get_verbose_name()))
    232          QtCore.QAbstractTableModel.__init__(self)
    233          self.admin = admin
    234          self.iconSize = QtCore.QSize( QtGui.QFontMetrics( self._header_font_required ).height() - 4, QtGui.QFontMetrics( self._header_font_required ).height() - 4 )
    235          if self.header_icon:
    236              self.form_icon = QtCore.QVariant( self.header_icon.getQIcon().pixmap( self.iconSize ) )
    237          else:
    238              self.form_icon = QtCore.QVariant()
    239          self.validator = admin.create_validator( self )
    240          self.collection_getter = collection_getter
    241          self.column_count = 0
    242          self.flush_changes = flush_changes
    243          self.delegate_manager = None
    244          self.mt = get_model_thread()
    245          # Set database connection and load data

    246          self._rows = 0
    247          self._columns = []
    248          self.max_number_of_rows = max_number_of_rows
    249          self.cache = {Qt.DisplayRole         : fifo( 10 * self.max_number_of_rows ),

    250                        Qt.EditRole            : fifo( 10 * self.max_number_of_rows ),

    251                        Qt.ToolTipRole         : fifo( 10 * self.max_number_of_rows ),

    252                        Qt.BackgroundColorRole : fifo( 10 * self.max_number_of_rows ), }
    253          # The rows in the table for which a cache refill is under request

    254          self.rows_under_request = set()
    255          # The rows that have unflushed changes

    256          self.unflushed_rows = set()
    257          self._sort_and_filter = SortingRowMapper()
    258          # Set edits

    259          self.edits = edits or []
    260          self.rsh = get_signal_handler()
    261          self.rsh.connect( self.rsh,

    262                           self.rsh.entity_update_signal,

    263                           self.handleEntityUpdate )
    264          self.rsh.connect( self.rsh,

    265                           self.rsh.entity_delete_signal,

    266                           self.handleEntityDelete )
    267          self.rsh.connect( self.rsh,

    268                           self.rsh.entity_create_signal,

    269                           self.handleEntityCreate )
    270

    271          def get_columns():
    272              self._columns = columns_getter()
    273              return self._columns
    274

    275          post( get_columns, self.setColumns )
    276  #    # the initial collection might contain unflushed rows

    277          post( self.updateUnflushedRows )
    278  #    # in that way the number of rows is requested as well

    279          post( self.getRowCount, self.setRowCount )
    280          self.logger.debug( 'initialization finished' )
    281

    282 -    def get_validator(self):
    283          return self.validator
    284

    285 -    def map_to_source(self, sorted_row_number):
    286          """Converts a sorted row number to a row number of the source

    287          collection"""
    288          return self._sort_and_filter[sorted_row_number]
    289

    290      @model_function
    291 -    def updateUnflushedRows( self ):
    292          """Verify all rows to see if some of them should be added to the

    293          unflushed rows"""
    294          for i, e in enumerate( self.collection_getter() ):
    295              if hasattr(e, 'id') and not e.id:
    296                  self.unflushed_rows.add( i )
    297

    298 -    def hasUnflushedRows( self ):
    299          """The model has rows that have not been flushed to the database yet,

    300          because the row is invalid

    301          """
    302          has_unflushed_rows = ( len( self.unflushed_rows ) > 0 )
    303          self.logger.debug( 'hasUnflushed rows : %s' % has_unflushed_rows )
    304          return has_unflushed_rows
    305

    306      @model_function
    307 -    def getRowCount( self ):
    308          # make sure we don't count an object twice if it is twice

    309          # in the list, since this will drive the cache nuts

    310          rows = len( set( self.collection_getter() ) )
    311          return rows
    312

    313      @gui_function
    314 -    def revertRow( self, row ):
    315          def create_refresh_entity( row ):
    316

    317              @model_function
    318              def refresh_entity():
    319                  o = self._get_object( row )
    320                  elixir.session.refresh( o )
    321                  return row, o
    322

    323              return refresh_entity
    324

    325          post( create_refresh_entity( row ), self._revert_row )
    326

    327 -    def _revert_row(self, row_and_entity ):
    328          row, entity = row_and_entity
    329          self.handleRowUpdate( row )
    330          self.rsh.sendEntityUpdate( self, entity )
    331

    332      @gui_function
    333 -    def refresh( self ):
    334          post( self.getRowCount, self._refresh_content )
    335

    336      @gui_function
    337 -    def _refresh_content(self, rows ):
    338          self.cache = {Qt.DisplayRole         : fifo( 10 * self.max_number_of_rows ),

    339                        Qt.EditRole            : fifo( 10 * self.max_number_of_rows ),

    340                        Qt.ToolTipRole         : fifo( 10 * self.max_number_of_rows ),

    341                        Qt.BackgroundColorRole : fifo( 10 * self.max_number_of_rows ),}
    342          self.setRowCount( rows )
    343

    344 -    def set_collection_getter( self, collection_getter ):
    345          self.logger.debug('set collection getter')
    346          self.collection_getter = collection_getter
    347          self.refresh()
    348

    349 -    def get_collection_getter( self ):
    350          return self.collection_getter
    351

    352 -    def handleRowUpdate( self, row ):
    353          """Handles the update of a row when this row might be out of date"""
    354          self.cache[Qt.DisplayRole].delete_by_row( row )
    355          self.cache[Qt.EditRole].delete_by_row( row )
    356          self.cache[Qt.ToolTipRole].delete_by_row( row )
    357          self.cache[Qt.BackgroundColorRole].delete_by_row( row )
    358          sig = 'dataChanged(const QModelIndex &, const QModelIndex &)'
    359          self.emit( QtCore.SIGNAL( sig ),

    360                    self.index( row, 0 ),

    361                    self.index( row, self.column_count ) )
    362

    363 -    def handleEntityUpdate( self, sender, entity ):
    364          """Handles the entity signal, indicating that the model is out of date"""
    365          self.logger.debug( '%s %s received entity update signal' % \

    366                       ( self.__class__.__name__, self.admin.get_verbose_name() ) )
    367          if sender != self:
    368              try:
    369                  row = self.cache[Qt.DisplayRole].get_row_by_entity(entity)
    370              except KeyError:
    371                  self.logger.debug( 'entity not in cache' )
    372                  return
    373              #

    374              # Because the entity is updated, it might no longer be in our

    375              # collection, therefore, make sure we don't access the collection

    376              # to strip data of the entity

    377              #

    378              def create_entity_update(row, entity):
    379

    380                  def entity_update():
    381                      columns = self.getColumns()
    382                      self._add_data(columns, row, entity)
    383                      return ((row,0), (row,self.column_count))
    384

    385                  return entity_update
    386

    387              post(create_entity_update(row, entity), self._emit_changes)
    388          else:
    389              self.logger.debug( 'duplicate update' )
    390

    391 -    def handleEntityDelete( self, sender, entity ):
    392          """Handles the entity signal, indicating that the model is out of date"""
    393          self.logger.debug( 'received entity delete signal' )
    394          if sender != self:
    395              self.refresh()
    396

    397 -    def handleEntityCreate( self, sender, entity ):
    398          """Handles the entity signal, indicating that the model is out of date"""
    399          self.logger.debug( 'received entity create signal' )
    400          if sender != self:
    401              self.refresh()
    402

    403 -    def setRowCount( self, rows ):
    404          """Callback method to set the number of rows

    405          @param rows the new number of rows

    406          """
    407          self._rows = rows
    408          if not sip.isdeleted(self):
    409              self.emit( QtCore.SIGNAL( 'layoutChanged()' ) )
    410

    411 -    def getItemDelegate( self ):
    412          """:return: a DelegateManager for this model, or None if no DelegateManager yet available

    413          a DelegateManager will be available once the item_delegate_changed signal has been emitted"""
    414          self.logger.debug( 'getItemDelegate' )
    415          return self.delegate_manager
    416

    417 -    def getColumns( self ):
    418          """@return: the columns as set by the setColumns method"""
    419          return self._columns
    420

    421      @gui_function
    422 -    def setColumns( self, columns ):
    423          """Callback method to set the columns

    424

    425          @param columns a list with fields to be displayed of the form [('field_name', field_attributes), ...] as

    426          returned by the getColumns method of the ElixirAdmin class

    427          """
    428          self.logger.debug( 'setColumns' )
    429          self.column_count = len( columns )
    430          self._columns = columns
    431

    432          delegate_manager = delegates.DelegateManager()
    433          delegate_manager.set_columns_desc( columns )
    434

    435          # set a delegate for the vertical header

    436          delegate_manager.insertColumnDelegate( -1, delegates.PlainTextDelegate(parent = delegate_manager) )
    437

    438          #

    439          # this loop can take a while to complete, so processEvents is called regulary

    440          #

    441          for i, c in enumerate( columns ):
    442  #            if i%10==0:

    443  #                QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.ExcludeSocketNotifiers, 100)

    444              field_name = c[0]
    445              self.logger.debug( 'creating delegate for %s' % field_name )
    446              if 'delegate' in c[1]:
    447                  try:
    448                      delegate = c[1]['delegate']( parent = delegate_manager, **c[1] )
    449                  except Exception, e:
    450                      logger.error('ProgrammingError : could not create delegate for field %s'%field_name, exc_info=e)
    451                      delegate = delegates.PlainTextDelegate( parent = delegate_manager, **c[1] )
    452                  delegate_manager.insertColumnDelegate( i, delegate )
    453                  continue
    454              elif c[1]['python_type'] == str:
    455                  if c[1]['length']:
    456                      delegate = delegates.PlainTextDelegate( parent = delegate_manager, maxlength = c[1]['length'] )
    457                      delegate_manager.insertColumnDelegate( i, delegate )
    458                  else:
    459                      delegate = delegates.TextEditDelegate( parent = delegate_manager, **c[1] )
    460                      delegate_manager.insertColumnDelegate( i, delegate )
    461              else:
    462                  delegate = delegates.PlainTextDelegate(parent = delegate_manager)
    463                  delegate_manager.insertColumnDelegate( i, delegate )
    464

    465          # Only set the delegate manager when it is fully set up

    466          self.delegate_manager = delegate_manager
    467          if not sip.isdeleted( self ):
    468              self.emit( self.item_delegate_changed_signal )
    469              self.emit( QtCore.SIGNAL( 'layoutChanged()' ) )
    470

    471 -    def rowCount( self, index = None ):
    472          return self._rows
    473

    474 -    def columnCount( self, index = None ):
    475          return self.column_count
    476

    477      @gui_function
    478 -    def headerData( self, section, orientation, role ):
    479          """In case the columns have not been set yet, don't even try to get

    480          information out of them

    481          """
    482          if orientation == Qt.Horizontal:
    483              if section >= self.column_count:
    484                  return QtCore.QAbstractTableModel.headerData( self, section, orientation, role )
    485              c = self.getColumns()[section]
    486

    487              if role == Qt.DisplayRole:
    488                  return QtCore.QVariant( unicode(c[1]['name']) )
    489

    490              elif role == Qt.FontRole:
    491                  if ( 'nullable' in c[1] ) and \

    492                     ( c[1]['nullable'] == False ):
    493                      return QtCore.QVariant( self._header_font_required )
    494                  else:
    495                      return QtCore.QVariant( self._header_font )
    496

    497              elif role == Qt.SizeHintRole:
    498                  option = QtGui.QStyleOptionViewItem()
    499                  if self.delegate_manager:
    500                      editor_size = self.delegate_manager.sizeHint( option, self.index( 0, section ) )
    501                  else:
    502                      editor_size = QtCore.QSize(0, 0)
    503                  if 'minimal_column_width' in c[1]:
    504                      minimal_column_width = QtGui.QFontMetrics( self._header_font ).size( Qt.TextSingleLine, 'A' ).width()*c[1]['minimal_column_width']
    505                  else:
    506                      minimal_column_width = 100
    507                  editable = True
    508                  if 'editable' in c[1]:
    509                      editable = c[1]['editable']
    510                  label_size = QtGui.QFontMetrics( self._header_font_required ).size( Qt.TextSingleLine, unicode(c[1]['name']) + ' ' )
    511                  size = max( minimal_column_width, label_size.width() + 10 )
    512                  if editable:
    513                      size = max( size, editor_size.width() )
    514                  return QtCore.QVariant( QtCore.QSize( size, label_size.height() + 10 ) )
    515          else:
    516              if role == Qt.SizeHintRole:
    517                  height = self.iconSize.height() + 5
    518                  if self.header_icon:
    519                      return QtCore.QVariant( QtCore.QSize( self.iconSize.width() + 10, height ) )
    520                  else:
    521                      # if there is no icon, the line numbers will be displayed, so create some space for those

    522                      return QtCore.QVariant( QtCore.QSize( QtGui.QFontMetrics( self._header_font ).size( Qt.TextSingleLine, str(self._rows) ).width() + 10, height) )
    523              if role == Qt.DecorationRole:
    524                  return self.form_icon
    525  #      elif role == Qt.DisplayRole:

    526  #        return QtCore.QVariant()

    527          return QtCore.QAbstractTableModel.headerData( self, section, orientation, role )
    528

    529      @gui_function
    530 -    def sort( self, column, order ):
    531          """reimplementation of the QAbstractItemModel its sort function"""
    532

    533          def create_sort(column, order):
    534

    535              def sort():
    536                  unsorted_collection = [(i,o) for i,o in enumerate(self.collection_getter())]
    537                  key = lambda item:getattr(item[1], self._columns[column][0])
    538                  unsorted_collection.sort(key=key, reverse=order)
    539                  for j,(i,_o) in enumerate(unsorted_collection):
    540                      self._sort_and_filter[j] = i
    541                  return len(unsorted_collection)
    542

    543              return sort
    544

    545          post(create_sort(column, order), self._refresh_content)
    546

    547      @gui_function
    548 -    def data( self, index, role ):
    549          if not index.isValid() or \

    550             not ( 0 <= index.row() <= self.rowCount( index ) ) or \

    551             not ( 0 <= index.column() <= self.columnCount( index ) ):
    552              return QtCore.QVariant()
    553          if role in ( Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole,):
    554              data = self._get_row_data( index.row(), role )
    555              try:
    556                  value = data[index.column()]
    557                  if isinstance( value, DelayedProxy ):
    558                      value = value()
    559                      data[index.column()] = value
    560                  if isinstance( value, datetime.datetime ):
    561                      # Putting a python datetime into a QVariant and returning it to a PyObject seems

    562                      # to be buggy, therefor we chop the microseconds

    563                      if value:
    564                          value = QtCore.QDateTime(value.year, value.month, value.day, value.hour, value.minute, value.second)
    565                  self.logger.debug( 'get data for row %s;col %s; role %s : %s' % ( index.row(), index.column(), role, unicode( value ) ) )
    566              except KeyError:
    567                  self.logger.error( 'Programming error, could not find data of column %s in %s' % ( index.column(), str( data ) ) )
    568                  value = None
    569              return QtCore.QVariant( value )
    570          elif role == Qt.ForegroundRole:
    571              pass
    572          elif role == Qt.BackgroundRole:
    573              data = self._get_row_data( index.row(), role )
    574              try:
    575                  value = data[index.column()]
    576              except:
    577                  self.logger.error( 'Programming error, could not find data of column %s in %s' % ( index.column(), str( data ) ) )
    578                  value = None
    579              if value in (None, ValueLoading):
    580                  return QtCore.QVariant(QtGui.QColor('white'))
    581              else:
    582                  return QtCore.QVariant(value)
    583          return QtCore.QVariant()
    584

    585 -    def setData( self, index, value, role = Qt.EditRole ):
    586          """Value should be a function taking no arguments that returns the data to

    587          be set

    588

    589          This function will then be called in the model_thread

    590          """
    591          if role == Qt.EditRole:
    592

    593              flushed = ( index.row() not in self.unflushed_rows )
    594              self.unflushed_rows.add( index.row() )
    595

    596              def make_update_function( row, column, value ):
    597

    598                  @model_function
    599                  def update_model_and_cache():
    600                      attribute, field_attributes = self.getColumns()[column]
    601                      # if the field is not editable, don't waste any time and get out of here

    602                      if not field_attributes['editable']:
    603                          return False
    604

    605                      from sqlalchemy.exceptions import DatabaseError
    606                      from sqlalchemy import orm
    607                      new_value = value()
    608                      self.logger.debug( 'set data for row %s;col %s' % ( row, column ) )
    609

    610                      if new_value == ValueLoading:
    611                          return None
    612

    613                      o = self._get_object( row )
    614                      if not o:
    615                          # the object might have been deleted from the collection while the editor

    616                          # was still open

    617                          self.logger.debug( 'this object is no longer in the collection' )
    618                          try:
    619                              self.unflushed_rows.remove( row )
    620                          except KeyError:
    621                              pass
    622                          return
    623

    624                      old_value = getattr( o, attribute )
    625                      changed = ( new_value != old_value )
    626                      #

    627                      # In case the attribute is a OneToMany or ManyToMany, we cannot simply compare the

    628                      # old and new value to know if the object was changed, so we'll

    629                      # consider it changed anyway

    630                      #

    631                      direction = field_attributes.get( 'direction', None )
    632                      if direction in ( orm.interfaces.MANYTOMANY, orm.interfaces.ONETOMANY ):
    633                          changed = True
    634                      if changed and field_attributes['editable'] == True:
    635                          # update the model

    636                          model_updated = False
    637                          try:
    638                              setattr( o, attribute, new_value )
    639                              #

    640                              # setting this attribute, might trigger a default function to return a value,

    641                              # that was not returned before

    642                              #

    643                              self.admin.set_defaults( o, include_nullable_fields=False )
    644                              model_updated = True
    645                          except AttributeError, e:
    646                              self.logger.error( u"Can't set attribute %s to %s" % ( attribute, unicode( new_value ) ), exc_info = e )
    647                          except TypeError:
    648                              # type error can be raised in case we try to set to a collection

    649                              pass
    650                          # update the cache

    651                          row_data = strip_data_from_object( o, self.getColumns() )
    652                          self.cache[Qt.EditRole].add_data( row, o, row_data )
    653                          self.cache[Qt.ToolTipRole].add_data( row, o, tool_tips_from_object( o, self.getColumns()) )
    654                          self.cache[Qt.BackgroundColorRole].add_data( row, o, background_colors_from_object( o, self.getColumns()) )
    655                          self.cache[Qt.DisplayRole].add_data( row, o, stripped_data_to_unicode( row_data, o, self.getColumns() ) )
    656                          if self.flush_changes and self.validator.isValid( row ):
    657                              # save the state before the update

    658                              try:
    659                                  self.admin.flush( o )
    660                              except DatabaseError, e:
    661                                  #@todo: when flushing fails, the object should not be removed from the unflushed rows ??

    662                                  self.logger.error( 'Programming Error, could not flush object', exc_info = e )
    663                              try:
    664                                  self.unflushed_rows.remove( row )
    665                              except KeyError:
    666                                  pass
    667                              #

    668                              # we can only track history if the model was updated, and it was

    669                              # flushed before, otherwise it has no primary key yet

    670                              #

    671                              if model_updated and hasattr(o, 'id') and o.id:
    672                                  #

    673                                  # in case of images or relations, we cannot pickle them

    674                                  #

    675                                  if ( not 'Imag' in old_value.__class__.__name__ ) and not direction:
    676                                      from camelot.model.memento import BeforeUpdate
    677                                      from camelot.model.authentication import getCurrentAuthentication
    678                                      history = BeforeUpdate( model = unicode( self.admin.entity.__name__ ),

    679                                                             primary_key = o.id,

    680                                                             previous_attributes = {attribute:old_value},

    681                                                             authentication = getCurrentAuthentication() )
    682

    683                                      try:
    684                                          elixir.session.flush( [history] )
    685                                      except DatabaseError, e:
    686                                          self.logger.error( 'Programming Error, could not flush history', exc_info = e )
    687                          #@todo: update should only be sent remotely when flush was done

    688                          self.rsh.sendEntityUpdate( self, o )
    689                          return ( ( row, 0 ), ( row, len( self.getColumns() ) ) )
    690                      elif flushed:
    691                          self.logger.debug( 'old value equals new value, no need to flush this object' )
    692                          try:
    693                              self.unflushed_rows.remove( row )
    694                          except KeyError:
    695                              pass
    696

    697                  return update_model_and_cache
    698

    699              post( make_update_function( index.row(), index.column(), value ), self._emit_changes )
    700

    701          return True
    702

    703 -    def _emit_changes( self, region ):
    704          if region:
    705              self.emit( QtCore.SIGNAL( 'dataChanged(const QModelIndex &, const QModelIndex &)' ),

    706                         self.index( region[0][0], region[0][1] ), self.index( region[1][0], region[1][1] ) )
    707

    708 -    def flags( self, index ):
    709          flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
    710          if self.getColumns()[index.column()][1]['editable']:
    711              flags = flags | Qt.ItemIsEditable
    712          return flags
    713

    714 -    def _add_data(self, columns, row, o):
    715          """Add data from object o at a row in the cache

    716          :param columns: the columns of which to strip data

    717          :param row: the row in the cache into which to add data

    718          :param o: the object from which to strip the data

    719          """
    720          row_data = strip_data_from_object( o, columns )
    721          self.cache[Qt.EditRole].add_data( row, o, row_data )
    722          self.cache[Qt.ToolTipRole].add_data( row, o, tool_tips_from_object( o, self.getColumns()) )
    723          self.cache[Qt.BackgroundColorRole].add_data( row, o, background_colors_from_object( o, self.getColumns()) )
    724          self.cache[Qt.DisplayRole].add_data( row, o, stripped_data_to_unicode( row_data, o, columns ) )
    725

    726

    727 -    def _skip_row(self, row, o):
    728          """:return: True if the object o is allready in the cache, but at a different row

    729          then row.  If this is the case, this object should not be put in the cache at row,

    730          and this row should be skipped alltogether.

    731          """
    732          try:
    733              return self.cache[Qt.EditRole].get_row_by_entity(o)!=row
    734          except KeyError:
    735              pass
    736          return False
    737

    738      @model_function
    739 -    def _extend_cache( self, offset, limit ):
    740          """Extend the cache around row"""
    741          columns = self.getColumns()
    742          offset = min( offset, self._rows )
    743          limit = min( limit, self._rows - offset )
    744          collection = self.collection_getter()
    745          skipped_rows = 0
    746          for i in range(offset, min(offset + limit + 1, self._rows)):
    747              object_found = False
    748              while not object_found:
    749                  unsorted_row = self._sort_and_filter[i]
    750                  obj = collection[unsorted_row+skipped_rows]
    751                  if self._skip_row(i, obj):
    752                      skipped_rows = skipped_rows + 1
    753                  else:
    754                      self._add_data(columns, i, obj)
    755                      object_found = True
    756          return ( offset, limit )
    757

    758      @model_function
    759 -    def _get_object( self, sorted_row_number ):
    760          """Get the object corresponding to row

    761          :return: the object at row row or None if the row index is invalid

    762          """
    763          try:
    764              # first try to get the primary key out of the cache, if it's not

    765              # there, query the collection_getter

    766              return self.cache[Qt.EditRole].get_entity_at_row( sorted_row_number )
    767          except KeyError:
    768              pass
    769          try:
    770              return self.collection_getter()[self.map_to_source(sorted_row_number)]
    771          except IndexError:
    772              pass
    773          return None
    774

    775 -    def _cache_extended( self, interval ):
    776          offset, limit = interval
    777          self.rows_under_request.difference_update( set( range( offset, offset + limit ) ) )
    778          self.emit( QtCore.SIGNAL( 'dataChanged(const QModelIndex &, const QModelIndex &)' ),

    779                    self.index( offset, 0 ), self.index( offset + limit, self.column_count ) )
    780

    781 -    def _get_row_data( self, row, role ):
    782          """Get the data which is to be visualized at a certain row of the

    783          table, if needed, post a refill request the cache to get the object

    784          and its neighbours in the cache, meanwhile, return an empty object

    785          @param role: Qt.EditRole or Qt.DisplayRole

    786          """
    787          role_cache = self.cache[role]
    788          try:
    789              return role_cache.get_data_at_row( row )
    790          except KeyError:
    791              if row not in self.rows_under_request:
    792                  offset = max( row - self.max_number_of_rows / 2, 0 )
    793                  limit = self.max_number_of_rows
    794                  self.rows_under_request.update( set( range( offset, offset + limit ) ) )
    795                  post( lambda :self._extend_cache( offset, limit ), self._cache_extended )
    796              return empty_row_data
    797

    798      @model_function
    799 -    def remove( self, o ):
    800          self.collection_getter().remove( o )
    801          self._rows -= 1
    802

    803      @model_function
    804 -    def append( self, o ):
    805          self.collection_getter().append( o )
    806          self._rows += 1
    807

    808      @model_function
    809 -    def removeEntityInstance( self, o, delete = True ):
    810          """Remove the entity instance o from this collection

    811          @param o: the object to be removed from this collection

    812          @param delete: delete the object after removing it from the collection

    813          """
    814          self.logger.debug( 'remove entity instance')
    815          self.remove( o )
    816          # remove the entity from the cache

    817          self.cache[Qt.DisplayRole].delete_by_entity( o )
    818          self.cache[Qt.ToolTipRole].delete_by_entity( o )
    819          self.cache[Qt.BackgroundColorRole].delete_by_entity( o )
    820          self.cache[Qt.EditRole].delete_by_entity( o )
    821          if delete:
    822              self.rsh.sendEntityDelete( self, o )
    823              self.admin.delete( o )
    824          else:
    825              # even if the object is not deleted, it needs to be flushed to make

    826              # sure it's out of the collection

    827              self.admin.flush( o )
    828          post( self.getRowCount, self._refresh_content )
    829

    830      @gui_function
    831 -    def removeRow( self, row, delete = True ):
    832          """Remove the entity associated with this row from this collection

    833          @param delete: delete the entity as well

    834          """
    835          self.logger.debug( 'remove row %s' % row )
    836

    837          def create_delete_function( row ):
    838

    839              def delete_function():
    840                  o = self._get_object( row )
    841                  if o:
    842                      self.removeEntityInstance( o, delete )
    843                  else:
    844                      # The object is not in this collection, maybe

    845                      # it was allready deleted, issue a refresh anyway

    846                      post( self.getRowCount, self._refresh_content )
    847

    848              return delete_function
    849

    850          post( create_delete_function( row ) )
    851          return True
    852

    853      @gui_function
    854 -    def copy_row( self, row ):
    855          """Copy the entity associated with this row to the end of the collection

    856          :param row: the row number

    857          """
    858

    859          def create_copy_function( row ):
    860

    861              def copy_function():
    862                  o = self._get_object(row)
    863                  new_object = self.admin.copy( o )
    864                  self.insertEntityInstance(self.getRowCount(), new_object)
    865

    866              return copy_function
    867

    868          post( create_copy_function( row ) )
    869          return True
    870

    871      @model_function
    872 -    def insertEntityInstance( self, row, o ):
    873          """Insert object o into this collection

    874          :param o: the object to be added to the collection

    875          :return: the row at which the object was inserted

    876          """
    877          self.append( o )
    878          row = self.getRowCount() - 1
    879          self.unflushed_rows.add( row )
    880          if self.flush_changes and not len( self.validator.objectValidity( o ) ):
    881              elixir.session.flush( [o] )
    882              try:
    883                  self.unflushed_rows.remove( row )
    884              except KeyError:
    885                  pass
    886  # TODO : it's not because an object is added to this list, that it was created

    887  # it might as well exist allready, eg. manytomany relation

    888  #      from camelot.model.memento import Create

    889  #      from camelot.model.authentication import getCurrentAuthentication

    890  #      history = Create(model=unicode(self.admin.entity.__name__),

    891  #                       primary_key=o.id,

    892  #                       authentication = getCurrentAuthentication())

    893  #      elixir.session.flush([history])

    894  #      self.rsh.sendEntityCreate(self, o)

    895          post( self.getRowCount, self._refresh_content )
    896          return row
    897

    898      @gui_function
    899 -    def insertRow( self, row, entity_instance_getter ):
    900

    901          def create_insert_function( getter ):
    902

    903              @model_function
    904              def insert_function():
    905                  self.insertEntityInstance( row, getter() )
    906

    907              return insert_function
    908

    909          post( create_insert_function( entity_instance_getter ) )
    910

    911      @model_function
    912 -    def getData( self ):
    913          """Generator for all the data queried by this proxy"""
    914          for _i, o in enumerate( self.collection_getter() ):
    915              yield strip_data_from_object( o, self.getColumns() )
    916

    917 -    def get_admin( self ):
    918          """Get the admin object associated with this model"""
    919          return self.admin
    920