| 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 sqlalchemy.orm.session import Session
|
|---|
| 45 | from camelot.view.art import Icon
|
|---|
| 46 | from camelot.view.fifo import fifo
|
|---|
| 47 | from camelot.view.controls import delegates
|
|---|
| 48 | from camelot.view.remote_signals import get_signal_handler
|
|---|
| 49 | from camelot.view.model_thread import gui_function, \
|
|---|
| 50 | model_function, post
|
|---|
| 51 |
|
|---|
| 52 |
|
|---|
| 53 | class DelayedProxy( object ):
|
|---|
| 54 | """A proxy object needs to be constructed within the GUI thread. Construct
|
|---|
| 55 | a delayed proxy when the construction of a proxy is needed within the Model
|
|---|
| 56 | thread. On first occasion the delayed proxy will be converted to a real
|
|---|
| 57 | proxy within the GUI thread
|
|---|
| 58 | """
|
|---|
| 59 |
|
|---|
| 60 | @model_function
|
|---|
| 61 | def __init__( self, *args, **kwargs ):
|
|---|
| 62 | self.args = args
|
|---|
| 63 | self.kwargs = kwargs
|
|---|
| 64 |
|
|---|
| 65 | @gui_function
|
|---|
| 66 | def __call__( self ):
|
|---|
| 67 | return CollectionProxy( *self.args, **self.kwargs )
|
|---|
| 68 |
|
|---|
| 69 | @model_function
|
|---|
| 70 | def tool_tips_from_object(obj, columns):
|
|---|
| 71 |
|
|---|
| 72 | data = []
|
|---|
| 73 |
|
|---|
| 74 | for col in columns:
|
|---|
| 75 | tooltip_getter = col[1]['tooltip']
|
|---|
| 76 | if tooltip_getter:
|
|---|
| 77 | data.append( tooltip_getter(obj) )
|
|---|
| 78 | else:
|
|---|
| 79 | data.append( None )
|
|---|
| 80 |
|
|---|
| 81 | return data
|
|---|
| 82 |
|
|---|
| 83 | @model_function
|
|---|
| 84 | def background_colors_from_object(obj, columns):
|
|---|
| 85 |
|
|---|
| 86 | data = []
|
|---|
| 87 |
|
|---|
| 88 | for col in columns:
|
|---|
| 89 | background_color_getter = col[1]['background_color']
|
|---|
| 90 | if background_color_getter:
|
|---|
| 91 | background_color = background_color_getter(obj)
|
|---|
| 92 | data.append( background_color )
|
|---|
| 93 | else:
|
|---|
| 94 | data.append( None )
|
|---|
| 95 |
|
|---|
| 96 | return data
|
|---|
| 97 |
|
|---|
| 98 | @model_function
|
|---|
| 99 | def strip_data_from_object( obj, columns ):
|
|---|
| 100 | """For every column in columns, get the corresponding value from the
|
|---|
| 101 | object. Getting a value from an object is time consuming, so using
|
|---|
| 102 | this function should be minimized.
|
|---|
| 103 | :param obj: the object of which to get data
|
|---|
| 104 | :param columns: a list of columns for which to get data
|
|---|
| 105 | """
|
|---|
| 106 | row_data = []
|
|---|
| 107 |
|
|---|
| 108 | def create_collection_getter( o, attr ):
|
|---|
| 109 | return lambda: getattr( o, attr )
|
|---|
| 110 |
|
|---|
| 111 | for _i, col in enumerate( columns ):
|
|---|
| 112 | field_attributes = col[1]
|
|---|
| 113 | if field_attributes['python_type'] == list:
|
|---|
| 114 | row_data.append( DelayedProxy( field_attributes['admin'],
|
|---|
| 115 | create_collection_getter( obj, col[0] ),
|
|---|
| 116 | field_attributes['admin'].get_columns ) )
|
|---|
| 117 | else:
|
|---|
| 118 | try:
|
|---|
| 119 | row_data.append( getattr( obj, col[0] ) )
|
|---|
| 120 | except Exception, e:
|
|---|
| 121 | logger.error('ProgrammingError : could not get attribute %s of object of type %s'%(col[0], obj.__class__.__name__),
|
|---|
| 122 | exc_info=e)
|
|---|
| 123 | row_data.append( None )
|
|---|
| 124 | return row_data
|
|---|
| 125 |
|
|---|
| 126 | @model_function
|
|---|
| 127 | def stripped_data_to_unicode( stripped_data, obj, columns ):
|
|---|
| 128 | """Extract for each field in the row data a 'visible' form of
|
|---|
| 129 | data"""
|
|---|
| 130 |
|
|---|
| 131 | row_data = []
|
|---|
| 132 |
|
|---|
| 133 | for field_data, ( _field_name, field_attributes ) in zip( stripped_data, columns ):
|
|---|
| 134 | unicode_data = u''
|
|---|
| 135 | if 'unicode_format' in field_attributes:
|
|---|
| 136 | unicode_format = field_attributes['unicode_format']
|
|---|
| 137 | if field_data != None:
|
|---|
| 138 | unicode_data = unicode_format( field_data )
|
|---|
| 139 | elif 'choices' in field_attributes:
|
|---|
| 140 | choices = field_attributes['choices']
|
|---|
| 141 | if callable(choices):
|
|---|
| 142 | for key, value in choices( obj ):
|
|---|
| 143 | if key == field_data:
|
|---|
| 144 | unicode_data = value
|
|---|
| 145 | continue
|
|---|
| 146 | else:
|
|---|
| 147 | unicode_data = field_data
|
|---|
| 148 | elif isinstance( field_data, DelayedProxy ):
|
|---|
| 149 | unicode_data = u'...'
|
|---|
| 150 | elif isinstance( field_data, list ):
|
|---|
| 151 | unicode_data = u'.'.join( [unicode( e ) for e in field_data] )
|
|---|
| 152 | elif isinstance( field_data, datetime.datetime ):
|
|---|
| 153 | # datetime should come before date since datetime is a subtype of date
|
|---|
| 154 | if field_data.year >= 1900:
|
|---|
| 155 | unicode_data = field_data.strftime( '%d/%m/%Y %H:%M' )
|
|---|
| 156 | elif isinstance( field_data, datetime.date ):
|
|---|
| 157 | if field_data.year >= 1900:
|
|---|
| 158 | unicode_data = field_data.strftime( '%d/%m/%Y' )
|
|---|
| 159 | elif field_data != None:
|
|---|
| 160 | unicode_data = unicode( field_data )
|
|---|
| 161 | row_data.append( unicode_data )
|
|---|
| 162 |
|
|---|
| 163 | return row_data
|
|---|
| 164 |
|
|---|
| 165 | from camelot.view.proxy import ValueLoading
|
|---|
| 166 |
|
|---|
| 167 | class EmptyRowData( object ):
|
|---|
| 168 | def __getitem__( self, column ):
|
|---|
| 169 | return ValueLoading
|
|---|
| 170 | return None
|
|---|
| 171 |
|
|---|
| 172 | empty_row_data = EmptyRowData()
|
|---|
| 173 |
|
|---|
| 174 | class SortingRowMapper( dict ):
|
|---|
| 175 | """Class mapping rows of a collection 1:1 without sorting
|
|---|
| 176 | and filtering, unless a mapping has been defined explicitly"""
|
|---|
| 177 |
|
|---|
| 178 | def __getitem__(self, row):
|
|---|
| 179 | try:
|
|---|
| 180 | return super(SortingRowMapper, self).__getitem__(row)
|
|---|
| 181 | except KeyError, _e:
|
|---|
| 182 | return row
|
|---|
| 183 |
|
|---|
| 184 | class CollectionProxy( QtCore.QAbstractTableModel ):
|
|---|
| 185 | """The CollectionProxy contains a limited copy of the data in the actual
|
|---|
| 186 | collection, usable for fast visualisation in a QTableView
|
|---|
| 187 | """
|
|---|
| 188 |
|
|---|
| 189 | _header_font = QtGui.QApplication.font()
|
|---|
| 190 | _header_font_required = QtGui.QApplication.font()
|
|---|
| 191 | _header_font_required.setBold( True )
|
|---|
| 192 |
|
|---|
| 193 | header_icon = Icon( 'tango/16x16/places/folder.png' )
|
|---|
| 194 |
|
|---|
| 195 | item_delegate_changed_signal = QtCore.SIGNAL('itemDelegateChanged')
|
|---|
| 196 |
|
|---|
| 197 | @gui_function
|
|---|
| 198 | def __init__( self, admin, collection_getter, columns_getter,
|
|---|
| 199 | max_number_of_rows = 10, edits = None, flush_changes = True ):
|
|---|
| 200 | """@param admin: the admin interface for the items in the collection
|
|---|
| 201 |
|
|---|
| 202 | @param collection_getter: a function that takes no arguments and returns
|
|---|
| 203 | the collection that will be visualized. This function will be called inside
|
|---|
| 204 | the model thread, to prevent delays when this function causes the database
|
|---|
| 205 | to be hit.
|
|---|
| 206 |
|
|---|
| 207 | @param columns_getter: a function that takes no arguments and returns the
|
|---|
| 208 | columns that will be cached in the proxy. This function will be called
|
|---|
| 209 | inside the model thread.
|
|---|
| 210 | """
|
|---|
| 211 | from camelot.view.model_thread import get_model_thread
|
|---|
| 212 | self.logger = logging.getLogger(logger.name + '.%s'%id(self))
|
|---|
| 213 | self.logger.debug('initialize query table for %s' % (admin.get_verbose_name()))
|
|---|
| 214 | QtCore.QAbstractTableModel.__init__(self)
|
|---|
| 215 | self.admin = admin
|
|---|
| 216 | self.iconSize = QtCore.QSize( QtGui.QFontMetrics( self._header_font_required ).height() - 4, QtGui.QFontMetrics( self._header_font_required ).height() - 4 )
|
|---|
| 217 | self.form_icon = QtCore.QVariant( self.header_icon.getQIcon().pixmap( self.iconSize ) )
|
|---|
| 218 | self.validator = admin.create_validator( self )
|
|---|
| 219 | self.collection_getter = collection_getter
|
|---|
| 220 | self.column_count = 0
|
|---|
| 221 | self.flush_changes = flush_changes
|
|---|
| 222 | self.delegate_manager = None
|
|---|
| 223 | self.mt = get_model_thread()
|
|---|
| 224 | # Set database connection and load data
|
|---|
| 225 | self.rows = 0
|
|---|
| 226 | self._columns = []
|
|---|
| 227 | self.max_number_of_rows = max_number_of_rows
|
|---|
| 228 | self.cache = {Qt.DisplayRole : fifo( 10 * self.max_number_of_rows ),
|
|---|
| 229 | Qt.EditRole : fifo( 10 * self.max_number_of_rows ),
|
|---|
| 230 | Qt.ToolTipRole : fifo( 10 * self.max_number_of_rows ),
|
|---|
| 231 | Qt.BackgroundColorRole : fifo( 10 * self.max_number_of_rows ), }
|
|---|
| 232 | # The rows in the table for which a cache refill is under request
|
|---|
| 233 | self.rows_under_request = set()
|
|---|
| 234 | # The rows that have unflushed changes
|
|---|
| 235 | self.unflushed_rows = set()
|
|---|
| 236 | self._sort_and_filter = SortingRowMapper()
|
|---|
| 237 | # Set edits
|
|---|
| 238 | self.edits = edits or []
|
|---|
| 239 | self.rsh = get_signal_handler()
|
|---|
| 240 | self.rsh.connect( self.rsh,
|
|---|
| 241 | self.rsh.entity_update_signal,
|
|---|
| 242 | self.handleEntityUpdate )
|
|---|
| 243 | self.rsh.connect( self.rsh,
|
|---|
| 244 | self.rsh.entity_delete_signal,
|
|---|
| 245 | self.handleEntityDelete )
|
|---|
| 246 | self.rsh.connect( self.rsh,
|
|---|
| 247 | self.rsh.entity_create_signal,
|
|---|
| 248 | self.handleEntityCreate )
|
|---|
| 249 |
|
|---|
| 250 | def get_columns():
|
|---|
| 251 | self._columns = columns_getter()
|
|---|
| 252 | return self._columns
|
|---|
| 253 |
|
|---|
| 254 | post( get_columns, self.setColumns )
|
|---|
| 255 | # # the initial collection might contain unflushed rows
|
|---|
| 256 | post( self.updateUnflushedRows )
|
|---|
| 257 | # # in that way the number of rows is requested as well
|
|---|
| 258 | post( self.getRowCount, self.setRowCount )
|
|---|
| 259 | self.logger.debug( 'initialization finished' )
|
|---|
| 260 |
|
|---|
| 261 | def map_to_source(self, sorted_row_number):
|
|---|
| 262 | """Converts a sorted row number to a row number of the source
|
|---|
| 263 | collection"""
|
|---|
| 264 | return self._sort_and_filter[sorted_row_number]
|
|---|
| 265 |
|
|---|
| 266 | @model_function
|
|---|
| 267 | def updateUnflushedRows( self ):
|
|---|
| 268 | """Verify all rows to see if some of them should be added to the
|
|---|
| 269 | unflushed rows"""
|
|---|
| 270 | for i, e in enumerate( self.collection_getter() ):
|
|---|
| 271 | if hasattr(e, 'id') and not e.id:
|
|---|
| 272 | self.unflushed_rows.add( i )
|
|---|
| 273 |
|
|---|
| 274 | def hasUnflushedRows( self ):
|
|---|
| 275 | """The model has rows that have not been flushed to the database yet,
|
|---|
| 276 | because the row is invalid
|
|---|
| 277 | """
|
|---|
| 278 | has_unflushed_rows = ( len( self.unflushed_rows ) > 0 )
|
|---|
| 279 | self.logger.debug( 'hasUnflushed rows : %s' % has_unflushed_rows )
|
|---|
| 280 | return has_unflushed_rows
|
|---|
| 281 |
|
|---|
| 282 | @model_function
|
|---|
| 283 | def getRowCount( self ):
|
|---|
| 284 | return len( self.collection_getter() )
|
|---|
| 285 |
|
|---|
| 286 | @gui_function
|
|---|
| 287 | def revertRow( self, row ):
|
|---|
| 288 | def create_refresh_entity( row ):
|
|---|
| 289 |
|
|---|
| 290 | @model_function
|
|---|
| 291 | def refresh_entity():
|
|---|
| 292 | o = self._get_object( row )
|
|---|
| 293 | elixir.session.refresh( o )
|
|---|
| 294 | return row, o
|
|---|
| 295 |
|
|---|
| 296 | return refresh_entity
|
|---|
| 297 |
|
|---|
| 298 | post( create_refresh_entity( row ), self._revert_row )
|
|---|
| 299 |
|
|---|
| 300 | def _revert_row(self, row_and_entity ):
|
|---|
| 301 | row, entity = row_and_entity
|
|---|
| 302 | self.handleRowUpdate( row )
|
|---|
| 303 | self.rsh.sendEntityUpdate( self, entity )
|
|---|
| 304 |
|
|---|
| 305 | @gui_function
|
|---|
| 306 | def refresh( self ):
|
|---|
| 307 | post( self.getRowCount, self._refresh_content )
|
|---|
| 308 |
|
|---|
| 309 | @gui_function
|
|---|
| 310 | def _refresh_content(self, rows ):
|
|---|
| 311 | self.cache = {Qt.DisplayRole : fifo( 10 * self.max_number_of_rows ),
|
|---|
| 312 | Qt.EditRole : fifo( 10 * self.max_number_of_rows ),
|
|---|
| 313 | Qt.ToolTipRole : fifo( 10 * self.max_number_of_rows ),
|
|---|
| 314 | Qt.BackgroundColorRole : fifo( 10 * self.max_number_of_rows ),}
|
|---|
| 315 | self.setRowCount( rows )
|
|---|
| 316 |
|
|---|
| 317 | def set_collection_getter( self, collection_getter ):
|
|---|
| 318 | self.collection_getter = collection_getter
|
|---|
| 319 | self.refresh()
|
|---|
| 320 |
|
|---|
| 321 | def get_collection_getter( self ):
|
|---|
| 322 | return self.collection_getter
|
|---|
| 323 |
|
|---|
| 324 | def handleRowUpdate( self, row ):
|
|---|
| 325 | """Handles the update of a row when this row might be out of date"""
|
|---|
| 326 | self.cache[Qt.DisplayRole].delete_by_row( row )
|
|---|
| 327 | self.cache[Qt.EditRole].delete_by_row( row )
|
|---|
| 328 | self.cache[Qt.ToolTipRole].delete_by_row( row )
|
|---|
| 329 | self.cache[Qt.BackgroundColorRole].delete_by_row( row )
|
|---|
| 330 | sig = 'dataChanged(const QModelIndex &, const QModelIndex &)'
|
|---|
| 331 | self.emit( QtCore.SIGNAL( sig ),
|
|---|
| 332 | self.index( row, 0 ),
|
|---|
| 333 | self.index( row, self.column_count ) )
|
|---|
| 334 |
|
|---|
| 335 | def handleEntityUpdate( self, sender, entity ):
|
|---|
| 336 | """Handles the entity signal, indicating that the model is out of date"""
|
|---|
| 337 | self.logger.debug( '%s %s received entity update signal' % \
|
|---|
| 338 | ( self.__class__.__name__, self.admin.get_verbose_name() ) )
|
|---|
| 339 | if sender != self:
|
|---|
| 340 | try:
|
|---|
| 341 | row = self.cache[Qt.DisplayRole].get_row_by_entity(entity)
|
|---|
| 342 | except KeyError:
|
|---|
| 343 | self.logger.debug( 'entity not in cache' )
|
|---|
| 344 | return
|
|---|
| 345 | #
|
|---|
| 346 | # Because the entity is updated, it might no longer be in our
|
|---|
| 347 | # collection, therefore, make sure we don't access the collection
|
|---|
| 348 | # to strip data of the entity
|
|---|
| 349 | #
|
|---|
| 350 | def create_entity_update(row, entity):
|
|---|
| 351 |
|
|---|
| 352 | def entity_update():
|
|---|
| 353 | columns = self.getColumns()
|
|---|
| 354 | self._add_data(columns, row, entity)
|
|---|
| 355 | return ((row,0), (row,self.column_count))
|
|---|
| 356 |
|
|---|
| 357 | return entity_update
|
|---|
| 358 |
|
|---|
| 359 | post(create_entity_update(row, entity), self._emit_changes)
|
|---|
| 360 | else:
|
|---|
| 361 | self.logger.debug( 'duplicate update' )
|
|---|
| 362 |
|
|---|
| 363 | def handleEntityDelete( self, sender, entity ):
|
|---|
| 364 | """Handles the entity signal, indicating that the model is out of date"""
|
|---|
| 365 | self.logger.debug( 'received entity delete signal' )
|
|---|
| 366 | if sender != self:
|
|---|
| 367 | self.refresh()
|
|---|
| 368 |
|
|---|
| 369 | def handleEntityCreate( self, sender, entity ):
|
|---|
| 370 | """Handles the entity signal, indicating that the model is out of date"""
|
|---|
| 371 | self.logger.debug( 'received entity create signal' )
|
|---|
| 372 | if sender != self:
|
|---|
| 373 | self.refresh()
|
|---|
| 374 |
|
|---|
| 375 | def setRowCount( self, rows ):
|
|---|
| 376 | """Callback method to set the number of rows
|
|---|
| 377 | @param rows the new number of rows
|
|---|
| 378 | """
|
|---|
| 379 | self.rows = rows
|
|---|
| 380 | self.emit( QtCore.SIGNAL( 'layoutChanged()' ) )
|
|---|
| 381 |
|
|---|
| 382 | def getItemDelegate( self ):
|
|---|
| 383 | """:return: a DelegateManager for this model, or None if no DelegateManager yet available
|
|---|
| 384 | a DelegateManager will be available once the item_delegate_changed signal has been emitted"""
|
|---|
| 385 | self.logger.debug( 'getItemDelegate' )
|
|---|
| 386 | return self.delegate_manager
|
|---|
| 387 |
|
|---|
| 388 | def getColumns( self ):
|
|---|
| 389 | """@return: the columns as set by the setColumns method"""
|
|---|
| 390 | return self._columns
|
|---|
| 391 |
|
|---|
| 392 | @gui_function
|
|---|
| 393 | def setColumns( self, columns ):
|
|---|
| 394 | """Callback method to set the columns
|
|---|
| 395 |
|
|---|
| 396 | @param columns a list with fields to be displayed of the form [('field_name', field_attributes), ...] as
|
|---|
| 397 | returned by the getColumns method of the ElixirAdmin class
|
|---|
| 398 | """
|
|---|
| 399 | self.logger.debug( 'setColumns' )
|
|---|
| 400 | self.column_count = len( columns )
|
|---|
| 401 | self._columns = columns
|
|---|
| 402 |
|
|---|
| 403 | delegate_manager = delegates.DelegateManager()
|
|---|
| 404 | delegate_manager.set_columns_desc( columns )
|
|---|
| 405 |
|
|---|
| 406 | # set a delegate for the vertical header
|
|---|
| 407 | delegate_manager.insertColumnDelegate( -1, delegates.PlainTextDelegate(parent = delegate_manager) )
|
|---|
| 408 |
|
|---|
| 409 | for i, c in enumerate( columns ):
|
|---|
| 410 | field_name = c[0]
|
|---|
| 411 | self.logger.debug( 'creating delegate for %s' % field_name )
|
|---|
| 412 | if 'delegate' in c[1]:
|
|---|
| 413 | try:
|
|---|
| 414 | delegate = c[1]['delegate']( parent = delegate_manager, **c[1] )
|
|---|
| 415 | except Exception, e:
|
|---|
| 416 | logger.error('ProgrammingError : could not create delegate for field %s'%field_name, exc_info=e)
|
|---|
| 417 | delegate = delegates.PlainTextDelegate( parent = delegate_manager, **c[1] )
|
|---|
| 418 | delegate_manager.insertColumnDelegate( i, delegate )
|
|---|
| 419 | continue
|
|---|
| 420 | elif c[1]['python_type'] == str:
|
|---|
| 421 | if c[1]['length']:
|
|---|
| 422 | delegate = delegates.PlainTextDelegate( parent = delegate_manager, maxlength = c[1]['length'] )
|
|---|
| 423 | delegate_manager.insertColumnDelegate( i, delegate )
|
|---|
| 424 | else:
|
|---|
| 425 | delegate = delegates.TextEditDelegate( parent = delegate_manager, **c[1] )
|
|---|
| 426 | delegate_manager.insertColumnDelegate( i, delegate )
|
|---|
| 427 | else:
|
|---|
| 428 | delegate = delegates.PlainTextDelegate(parent = delegate_manager)
|
|---|
| 429 | delegate_manager.insertColumnDelegate( i, delegate )
|
|---|
| 430 |
|
|---|
| 431 | # Only set the delegate manager when it is fully set up
|
|---|
| 432 | self.delegate_manager = delegate_manager
|
|---|
| 433 | if not sip.isdeleted( self ):
|
|---|
| 434 | self.emit( self.item_delegate_changed_signal )
|
|---|
| 435 | self.emit( QtCore.SIGNAL( 'layoutChanged()' ) )
|
|---|
| 436 |
|
|---|
| 437 | def rowCount( self, index = None ):
|
|---|
| 438 | return self.rows
|
|---|
| 439 |
|
|---|
| 440 | def columnCount( self, index = None ):
|
|---|
| 441 | return self.column_count
|
|---|
| 442 |
|
|---|
| 443 | @gui_function
|
|---|
| 444 | def headerData( self, section, orientation, role ):
|
|---|
| 445 | """In case the columns have not been set yet, don't even try to get
|
|---|
| 446 | information out of them
|
|---|
| 447 | """
|
|---|
| 448 | if orientation == Qt.Horizontal:
|
|---|
| 449 | if section >= self.column_count:
|
|---|
| 450 | return QtCore.QAbstractTableModel.headerData( self, section, orientation, role )
|
|---|
| 451 | c = self.getColumns()[section]
|
|---|
| 452 |
|
|---|
| 453 | if role == Qt.DisplayRole:
|
|---|
| 454 | return QtCore.QVariant( unicode(c[1]['name']) )
|
|---|
| 455 |
|
|---|
| 456 | elif role == Qt.FontRole:
|
|---|
| 457 | if ( 'nullable' in c[1] ) and \
|
|---|
| 458 | ( c[1]['nullable'] == False ):
|
|---|
| 459 | return QtCore.QVariant( self._header_font_required )
|
|---|
| 460 | else:
|
|---|
| 461 | return QtCore.QVariant( self._header_font )
|
|---|
| 462 |
|
|---|
| 463 | elif role == Qt.SizeHintRole:
|
|---|
| 464 | option = QtGui.QStyleOptionViewItem()
|
|---|
| 465 | if self.delegate_manager:
|
|---|
| 466 | editor_size = self.delegate_manager.sizeHint( option, self.index( 0, section ) )
|
|---|
| 467 | else:
|
|---|
| 468 | editor_size = 0
|
|---|
| 469 | if 'minimal_column_width' in c[1]:
|
|---|
| 470 | minimal_column_width = QtGui.QFontMetrics( self._header_font ).size( Qt.TextSingleLine, 'A' ).width()*c[1]['minimal_column_width']
|
|---|
| 471 | else:
|
|---|
| 472 | minimal_column_width = 0
|
|---|
| 473 | editable = True
|
|---|
| 474 | if 'editable' in c[1]:
|
|---|
| 475 | editable = c[1]['editable']
|
|---|
| 476 | label_size = QtGui.QFontMetrics( self._header_font_required ).size( Qt.TextSingleLine, unicode(c[1]['name']) + ' ' )
|
|---|
| 477 | size = max( minimal_column_width, label_size.width() + 10 )
|
|---|
| 478 | if editable:
|
|---|
| 479 | size = max( size, editor_size.width() )
|
|---|
| 480 | return QtCore.QVariant( QtCore.QSize( size, label_size.height() + 10 ) )
|
|---|
| 481 | else:
|
|---|
| 482 | if role == Qt.SizeHintRole:
|
|---|
| 483 | return QtCore.QVariant( QtCore.QSize( self.iconSize.width() + 8, self.iconSize.height() + 5 ) )
|
|---|
| 484 | if role == Qt.DecorationRole:
|
|---|
| 485 | return self.form_icon
|
|---|
| 486 | # elif role == Qt.DisplayRole:
|
|---|
| 487 | # return QtCore.QVariant()
|
|---|
| 488 | return QtCore.QAbstractTableModel.headerData( self, section, orientation, role )
|
|---|
| 489 |
|
|---|
| 490 | @gui_function
|
|---|
| 491 | def sort( self, column, order ):
|
|---|
| 492 | """reimplementation of the QAbstractItemModel its sort function"""
|
|---|
| 493 |
|
|---|
| 494 | def create_sort(column, order):
|
|---|
| 495 |
|
|---|
| 496 | def sort():
|
|---|
| 497 | unsorted_collection = [(i,o) for i,o in enumerate(self.collection_getter())]
|
|---|
| 498 | key = lambda item:getattr(item[1], self._columns[column][0])
|
|---|
| 499 | unsorted_collection.sort(key=key, reverse=order)
|
|---|
| 500 | for j,(i,_o) in enumerate(unsorted_collection):
|
|---|
| 501 | self._sort_and_filter[j] = i
|
|---|
| 502 | return len(unsorted_collection)
|
|---|
| 503 |
|
|---|
| 504 | return sort
|
|---|
| 505 |
|
|---|
| 506 | post(create_sort(column, order), self._refresh_content)
|
|---|
| 507 |
|
|---|
| 508 | @gui_function
|
|---|
| 509 | def data( self, index, role ):
|
|---|
| 510 | if not index.isValid() or \
|
|---|
| 511 | not ( 0 <= index.row() <= self.rowCount( index ) ) or \
|
|---|
| 512 | not ( 0 <= index.column() <= self.columnCount( index ) ):
|
|---|
| 513 | return QtCore.QVariant()
|
|---|
| 514 | if role in ( Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole,):
|
|---|
| 515 | data = self._get_row_data( index.row(), role )
|
|---|
| 516 | try:
|
|---|
| 517 | value = data[index.column()]
|
|---|
| 518 | if isinstance( value, DelayedProxy ):
|
|---|
| 519 | value = value()
|
|---|
| 520 | data[index.column()] = value
|
|---|
| 521 | if isinstance( value, datetime.datetime ):
|
|---|
| 522 | # Putting a python datetime into a QVariant and returning it to a PyObject seems
|
|---|
| 523 | # to be buggy, therefor we chop the microseconds
|
|---|
| 524 | if value:
|
|---|
| 525 | value = QtCore.QDateTime(value.year, value.month, value.day, value.hour, value.minute, value.second)
|
|---|
| 526 | self.logger.debug( 'get data for row %s;col %s; role %s : %s' % ( index.row(), index.column(), role, unicode( value ) ) )
|
|---|
| 527 | except KeyError:
|
|---|
| 528 | self.logger.error( 'Programming error, could not find data of column %s in %s' % ( index.column(), str( data ) ) )
|
|---|
| 529 | value = None
|
|---|
| 530 | return QtCore.QVariant( value )
|
|---|
| 531 | elif role == Qt.ForegroundRole:
|
|---|
| 532 | pass
|
|---|
| 533 | elif role == Qt.BackgroundRole:
|
|---|
| 534 | data = self._get_row_data( index.row(), role )
|
|---|
| 535 | try:
|
|---|
| 536 | value = data[index.column()]
|
|---|
| 537 | except:
|
|---|
| 538 | self.logger.error( 'Programming error, could not find data of column %s in %s' % ( index.column(), str( data ) ) )
|
|---|
| 539 | value = None
|
|---|
| 540 | if value in (None, ValueLoading):
|
|---|
| 541 | return QtCore.QVariant(QtGui.QColor('white'))
|
|---|
| 542 | else:
|
|---|
| 543 | return QtCore.QVariant(value)
|
|---|
| 544 | return QtCore.QVariant()
|
|---|
| 545 |
|
|---|
| 546 | def setData( self, index, value, role = Qt.EditRole ):
|
|---|
| 547 | """Value should be a function taking no arguments that returns the data to
|
|---|
| 548 | be set
|
|---|
| 549 |
|
|---|
| 550 | This function will then be called in the model_thread
|
|---|
| 551 | """
|
|---|
| 552 | if role == Qt.EditRole:
|
|---|
| 553 |
|
|---|
| 554 | flushed = ( index.row() not in self.unflushed_rows )
|
|---|
| 555 | self.unflushed_rows.add( index.row() )
|
|---|
| 556 |
|
|---|
| 557 | def make_update_function( row, column, value ):
|
|---|
| 558 |
|
|---|
| 559 | @model_function
|
|---|
| 560 | def update_model_and_cache():
|
|---|
| 561 | attribute, field_attributes = self.getColumns()[column]
|
|---|
| 562 | # if the field is not editable, don't waste any time and get out of here
|
|---|
| 563 | if not field_attributes['editable']:
|
|---|
| 564 | return False
|
|---|
| 565 |
|
|---|
| 566 | from sqlalchemy.exceptions import DatabaseError
|
|---|
| 567 | from sqlalchemy import orm
|
|---|
| 568 | new_value = value()
|
|---|
| 569 | self.logger.debug( 'set data for row %s;col %s' % ( row, column ) )
|
|---|
| 570 |
|
|---|
| 571 | if new_value == ValueLoading:
|
|---|
| 572 | return None
|
|---|
| 573 |
|
|---|
| 574 | o = self._get_object( row )
|
|---|
| 575 | if not o:
|
|---|
| 576 | # the object might have been deleted from the collection while the editor
|
|---|
| 577 | # was still open
|
|---|
| 578 | self.logger.debug( 'this object is no longer in the collection' )
|
|---|
| 579 | try:
|
|---|
| 580 | self.unflushed_rows.remove( row )
|
|---|
| 581 | except KeyError:
|
|---|
| 582 | pass
|
|---|
| 583 | return
|
|---|
| 584 |
|
|---|
| 585 | old_value = getattr( o, attribute )
|
|---|
| 586 | changed = ( new_value != old_value )
|
|---|
| 587 | #
|
|---|
| 588 | # In case the attribute is a OneToMany or ManyToMany, we cannot simply compare the
|
|---|
| 589 | # old and new value to know if the object was changed, so we'll
|
|---|
| 590 | # consider it changed anyway
|
|---|
| 591 | #
|
|---|
| 592 | direction = field_attributes.get( 'direction', None )
|
|---|
| 593 | if direction in ( orm.interfaces.MANYTOMANY, orm.interfaces.ONETOMANY ):
|
|---|
| 594 | changed = True
|
|---|
| 595 | if changed and field_attributes['editable'] == True:
|
|---|
| 596 | # update the model
|
|---|
| 597 | model_updated = False
|
|---|
| 598 | try:
|
|---|
| 599 | setattr( o, attribute, new_value )
|
|---|
| 600 | model_updated = True
|
|---|
| 601 | except AttributeError, e:
|
|---|
| 602 | self.logger.error( u"Can't set attribute %s to %s" % ( attribute, unicode( new_value ) ), exc_info = e )
|
|---|
| 603 | except TypeError:
|
|---|
| 604 | # type error can be raised in case we try to set to a collection
|
|---|
| 605 | pass
|
|---|
| 606 | # update the cache
|
|---|
| 607 | row_data = strip_data_from_object( o, self.getColumns() )
|
|---|
| 608 | self.cache[Qt.EditRole].add_data( row, o, row_data )
|
|---|
| 609 | self.cache[Qt.ToolTipRole].add_data( row, o, tool_tips_from_object( o, self.getColumns()) )
|
|---|
| 610 | self.cache[Qt.BackgroundColorRole].add_data( row, o, background_colors_from_object( o, self.getColumns()) )
|
|---|
| 611 | self.cache[Qt.DisplayRole].add_data( row, o, stripped_data_to_unicode( row_data, o, self.getColumns() ) )
|
|---|
| 612 | if self.flush_changes and self.validator.isValid( row ):
|
|---|
| 613 | # save the state before the update
|
|---|
| 614 | try:
|
|---|
| 615 | self.admin.flush( o )
|
|---|
| 616 | except DatabaseError, e:
|
|---|
| 617 | #@todo: when flushing fails, the object should not be removed from the unflushed rows ??
|
|---|
| 618 | self.logger.error( 'Programming Error, could not flush object', exc_info = e )
|
|---|
| 619 | try:
|
|---|
| 620 | self.unflushed_rows.remove( row )
|
|---|
| 621 | except KeyError:
|
|---|
| 622 | pass
|
|---|
| 623 | #
|
|---|
| 624 | # we can only track history if the model was updated, and it was
|
|---|
| 625 | # flushed before, otherwise it has no primary key yet
|
|---|
| 626 | #
|
|---|
| 627 | if model_updated and hasattr(o, 'id') and o.id:
|
|---|
| 628 | #
|
|---|
| 629 | # in case of images or relations, we cannot pickle them
|
|---|
| 630 | #
|
|---|
| 631 | if ( not 'Imag' in old_value.__class__.__name__ ) and not direction:
|
|---|
| 632 | from camelot.model.memento import BeforeUpdate
|
|---|
| 633 | from camelot.model.authentication import getCurrentAuthentication
|
|---|
| 634 | history = BeforeUpdate( model = unicode( self.admin.entity.__name__ ),
|
|---|
| 635 | primary_key = o.id,
|
|---|
| 636 | previous_attributes = {attribute:old_value},
|
|---|
| 637 | authentication = getCurrentAuthentication() )
|
|---|
| 638 |
|
|---|
| 639 | try:
|
|---|
| 640 | elixir.session.flush( [history] )
|
|---|
| 641 | except DatabaseError, e:
|
|---|
| 642 | self.logger.error( 'Programming Error, could not flush history', exc_info = e )
|
|---|
| 643 | #@todo: update should only be sent remotely when flush was done
|
|---|
| 644 | self.rsh.sendEntityUpdate( self, o )
|
|---|
| 645 | return ( ( row, 0 ), ( row, len( self.getColumns() ) ) )
|
|---|
| 646 | elif flushed:
|
|---|
| 647 | self.logger.debug( 'old value equals new value, no need to flush this object' )
|
|---|
| 648 | try:
|
|---|
| 649 | self.unflushed_rows.remove( row )
|
|---|
| 650 | except KeyError:
|
|---|
| 651 | pass
|
|---|
| 652 |
|
|---|
| 653 | return update_model_and_cache
|
|---|
| 654 |
|
|---|
| 655 | post( make_update_function( index.row(), index.column(), value ), self._emit_changes )
|
|---|
| 656 |
|
|---|
| 657 | return True
|
|---|
| 658 |
|
|---|
| 659 | def _emit_changes( self, region ):
|
|---|
| 660 | if region:
|
|---|
| 661 | self.emit( QtCore.SIGNAL( 'dataChanged(const QModelIndex &, const QModelIndex &)' ),
|
|---|
| 662 | self.index( region[0][0], region[0][1] ), self.index( region[1][0], region[1][1] ) )
|
|---|
| 663 |
|
|---|
| 664 | def flags( self, index ):
|
|---|
| 665 | flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
|---|
| 666 | if self.getColumns()[index.column()][1]['editable']:
|
|---|
| 667 | flags = flags | Qt.ItemIsEditable
|
|---|
| 668 | return flags
|
|---|
| 669 |
|
|---|
| 670 | def _add_data(self, columns, row, o):
|
|---|
| 671 | """Add data from object o at a row in the cache
|
|---|
| 672 | :param columns: the columns of which to strip data
|
|---|
| 673 | :param row: the row in the cache into which to add data
|
|---|
| 674 | :param o: the object from which to strip the data
|
|---|
| 675 | """
|
|---|
| 676 | row_data = strip_data_from_object( o, columns )
|
|---|
| 677 | self.cache[Qt.EditRole].add_data( row, o, row_data )
|
|---|
| 678 | self.cache[Qt.ToolTipRole].add_data( row, o, tool_tips_from_object( o, self.getColumns()) )
|
|---|
| 679 | self.cache[Qt.BackgroundColorRole].add_data( row, o, background_colors_from_object( o, self.getColumns()) )
|
|---|
| 680 | self.cache[Qt.DisplayRole].add_data( row, o, stripped_data_to_unicode( row_data, o, columns ) )
|
|---|
| 681 |
|
|---|
| 682 | @model_function
|
|---|
| 683 | def _extend_cache( self, offset, limit ):
|
|---|
| 684 | """Extend the cache around row"""
|
|---|
| 685 | columns = self.getColumns()
|
|---|
| 686 | offset = min( offset, self.rows )
|
|---|
| 687 | limit = min( limit, self.rows - offset )
|
|---|
| 688 | collection = self.collection_getter()
|
|---|
| 689 | # for i, o in enumerate( collection[offset:offset + limit + 1] ):
|
|---|
| 690 | # self._add_data(columns, i+offset, o)
|
|---|
| 691 | for i in range(offset, min(offset + limit + 1, len(collection))):
|
|---|
| 692 | unsorted_row = self._sort_and_filter[i]
|
|---|
| 693 | obj = collection[unsorted_row]
|
|---|
| 694 | self._add_data(columns, i, obj)
|
|---|
| 695 | return ( offset, limit )
|
|---|
| 696 |
|
|---|
| 697 | @model_function
|
|---|
| 698 | def _get_object( self, sorted_row_number ):
|
|---|
| 699 | """Get the object corresponding to row
|
|---|
| 700 | :return: the object at row row or None if the row index is invalid
|
|---|
| 701 | """
|
|---|
| 702 | try:
|
|---|
| 703 | # first try to get the primary key out of the cache, if it's not
|
|---|
| 704 | # there, query the collection_getter
|
|---|
| 705 | return self.cache[Qt.EditRole].get_entity_at_row( sorted_row_number )
|
|---|
| 706 | except KeyError:
|
|---|
| 707 | pass
|
|---|
| 708 | try:
|
|---|
| 709 | return self.collection_getter()[self.map_to_source(sorted_row_number)]
|
|---|
| 710 | except IndexError:
|
|---|
| 711 | pass
|
|---|
| 712 | return None
|
|---|
| 713 |
|
|---|
| 714 | def _cache_extended( self, interval ):
|
|---|
| 715 | offset, limit = interval
|
|---|
| 716 | self.rows_under_request.difference_update( set( range( offset, offset + limit ) ) )
|
|---|
| 717 | self.emit( QtCore.SIGNAL( 'dataChanged(const QModelIndex &, const QModelIndex &)' ),
|
|---|
| 718 | self.index( offset, 0 ), self.index( offset + limit, self.column_count ) )
|
|---|
| 719 |
|
|---|
| 720 | def _get_row_data( self, row, role ):
|
|---|
| 721 | """Get the data which is to be visualized at a certain row of the
|
|---|
| 722 | table, if needed, post a refill request the cache to get the object
|
|---|
| 723 | and its neighbours in the cache, meanwhile, return an empty object
|
|---|
| 724 | @param role: Qt.EditRole or Qt.DisplayRole
|
|---|
| 725 | """
|
|---|
| 726 | role_cache = self.cache[role]
|
|---|
| 727 | try:
|
|---|
| 728 | return role_cache.get_data_at_row( row )
|
|---|
| 729 | except KeyError:
|
|---|
| 730 | if row not in self.rows_under_request:
|
|---|
| 731 | offset = max( row - self.max_number_of_rows / 2, 0 )
|
|---|
| 732 | limit = self.max_number_of_rows
|
|---|
| 733 | self.rows_under_request.update( set( range( offset, offset + limit ) ) )
|
|---|
| 734 | post( lambda :self._extend_cache( offset, limit ), self._cache_extended )
|
|---|
| 735 | return empty_row_data
|
|---|
| 736 |
|
|---|
| 737 | @model_function
|
|---|
| 738 | def remove( self, o ):
|
|---|
| 739 | self.collection_getter().remove( o )
|
|---|
| 740 | self.rows -= 1
|
|---|
| 741 |
|
|---|
| 742 | @model_function
|
|---|
| 743 | def append( self, o ):
|
|---|
| 744 | self.collection_getter().append( o )
|
|---|
| 745 | self.rows += 1
|
|---|
| 746 |
|
|---|
| 747 | @model_function
|
|---|
| 748 | def removeEntityInstance( self, o, delete = True ):
|
|---|
| 749 | """Remove the entity instance o from this collection
|
|---|
| 750 | @param o: the object to be removed from this collection
|
|---|
| 751 | @param delete: delete the object after removing it from the collection
|
|---|
| 752 | """
|
|---|
| 753 | self.logger.debug( 'remove entity instance')
|
|---|
| 754 | self.remove( o )
|
|---|
| 755 | # remove the entity from the cache
|
|---|
| 756 | self.cache[Qt.DisplayRole].delete_by_entity( o )
|
|---|
| 757 | self.cache[Qt.ToolTipRole].delete_by_entity( o )
|
|---|
| 758 | self.cache[Qt.BackgroundColorRole].delete_by_entity( o )
|
|---|
| 759 | self.cache[Qt.EditRole].delete_by_entity( o )
|
|---|
| 760 | if delete:
|
|---|
| 761 | self.rsh.sendEntityDelete( self, o )
|
|---|
| 762 | self.admin.delete( o )
|
|---|
| 763 | else:
|
|---|
| 764 | # even if the object is not deleted, it needs to be flushed to make
|
|---|
| 765 | # sure it's out of the collection
|
|---|
| 766 | self.admin.flush( o )
|
|---|
| 767 | post( self.getRowCount, self._refresh_content )
|
|---|
| 768 |
|
|---|
| 769 | @gui_function
|
|---|
| 770 | def removeRow( self, row, delete = True ):
|
|---|
| 771 | """Remove the entity associated with this row from this collection
|
|---|
| 772 | @param delete: delete the entity as well
|
|---|
| 773 | """
|
|---|
| 774 | self.logger.debug( 'remove row %s' % row )
|
|---|
| 775 |
|
|---|
| 776 | def create_delete_function( row ):
|
|---|
| 777 |
|
|---|
| 778 | def delete_function():
|
|---|
| 779 | o = self._get_object( row )
|
|---|
| 780 | if o:
|
|---|
| 781 | self.removeEntityInstance( o, delete )
|
|---|
| 782 | else:
|
|---|
| 783 | # The object is not in this collection, maybe
|
|---|
| 784 | # it was allready deleted, issue a refresh anyway
|
|---|
| 785 | post( self.getRowCount, self._refresh_content )
|
|---|
| 786 |
|
|---|
| 787 | return delete_function
|
|---|
| 788 |
|
|---|
| 789 | post( create_delete_function( row ) )
|
|---|
| 790 | return True
|
|---|
| 791 |
|
|---|
| 792 | @gui_function
|
|---|
| 793 | def copy_row( self, row ):
|
|---|
| 794 | """Copy the entity associated with this row to the end of the collection
|
|---|
| 795 | :param row: the row number
|
|---|
| 796 | """
|
|---|
| 797 |
|
|---|
| 798 | def create_copy_function( row ):
|
|---|
| 799 |
|
|---|
| 800 | def copy_function():
|
|---|
| 801 | o = self._get_object(row)
|
|---|
| 802 | new_object = self.admin.copy( o )
|
|---|
| 803 | self.insertEntityInstance(self.getRowCount(), new_object)
|
|---|
| 804 |
|
|---|
| 805 | return copy_function
|
|---|
| 806 |
|
|---|
| 807 | post( create_copy_function( row ) )
|
|---|
| 808 | return True
|
|---|
| 809 |
|
|---|
| 810 | @model_function
|
|---|
| 811 | def insertEntityInstance( self, row, o ):
|
|---|
| 812 | """Insert object o into this collection
|
|---|
| 813 | :param o: the object to be added to the collection
|
|---|
| 814 | :return: the row at which the object was inserted
|
|---|
| 815 | """
|
|---|
| 816 | self.append( o )
|
|---|
| 817 | row = self.getRowCount() - 1
|
|---|
| 818 | self.unflushed_rows.add( row )
|
|---|
| 819 | if self.flush_changes and not len( self.validator.objectValidity( o ) ):
|
|---|
| 820 | elixir.session.flush( [o] )
|
|---|
| 821 | try:
|
|---|
| 822 | self.unflushed_rows.remove( row )
|
|---|
| 823 | except KeyError:
|
|---|
| 824 | pass
|
|---|
| 825 | # TODO : it's not because an object is added to this list, that it was created
|
|---|
| 826 | # it might as well exist allready, eg. manytomany relation
|
|---|
| 827 | # from camelot.model.memento import Create
|
|---|
| 828 | # from camelot.model.authentication import getCurrentAuthentication
|
|---|
| 829 | # history = Create(model=unicode(self.admin.entity.__name__),
|
|---|
| 830 | # primary_key=o.id,
|
|---|
| 831 | # authentication = getCurrentAuthentication())
|
|---|
| 832 | # elixir.session.flush([history])
|
|---|
| 833 | # self.rsh.sendEntityCreate(self, o)
|
|---|
| 834 | post( self.getRowCount, self._refresh_content )
|
|---|
| 835 | return row
|
|---|
| 836 |
|
|---|
| 837 | @gui_function
|
|---|
| 838 | def insertRow( self, row, entity_instance_getter ):
|
|---|
| 839 |
|
|---|
| 840 | def create_insert_function( getter ):
|
|---|
| 841 |
|
|---|
| 842 | @model_function
|
|---|
| 843 | def insert_function():
|
|---|
| 844 | self.insertEntityInstance( row, getter() )
|
|---|
| 845 |
|
|---|
| 846 | return insert_function
|
|---|
| 847 |
|
|---|
| 848 | post( create_insert_function( entity_instance_getter ) )
|
|---|
| 849 |
|
|---|
| 850 | @model_function
|
|---|
| 851 | def getData( self ):
|
|---|
| 852 | """Generator for all the data queried by this proxy"""
|
|---|
| 853 | for _i, o in enumerate( self.collection_getter() ):
|
|---|
| 854 | yield strip_data_from_object( o, self.getColumns() )
|
|---|
| 855 |
|
|---|
| 856 | def get_admin( self ):
|
|---|
| 857 | """Get the admin object associated with this model"""
|
|---|
| 858 | return self.admin
|
|---|
| 859 |
|
|---|
| 860 | def get_collection_getter(self):
|
|---|
| 861 | return self.collection_getter
|
|---|
| 862 |
|
|---|