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