1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
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
61 self.args = args
62 self.kwargs = kwargs
63
64 @gui_function
67
85
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
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
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
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
181
182 empty_row_data = EmptyRowData()
185 """Class mapping rows of a collection 1:1 without sorting
186 and filtering, unless a mapping has been defined explicitly"""
187
193
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
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
254 self.rows_under_request = set()
255
256 self.unflushed_rows = set()
257 self._sort_and_filter = SortingRowMapper()
258
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
277 post( self.updateUnflushedRows )
278
279 post( self.getRowCount, self.setRowCount )
280 self.logger.debug( 'initialization finished' )
281
284
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
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
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
308
309
310 rows = len( set( self.collection_getter() ) )
311 return rows
312
313 @gui_function
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
331
332 @gui_function
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
345 self.logger.debug('set collection getter')
346 self.collection_getter = collection_getter
347 self.refresh()
348
350 return self.collection_getter
351
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
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
375
376
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
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
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
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
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
418 """@return: the columns as set by the setColumns method"""
419 return self._columns
420
421 @gui_function
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
436 delegate_manager.insertColumnDelegate( -1, delegates.PlainTextDelegate(parent = delegate_manager) )
437
438
439
440
441 for i, c in enumerate( columns ):
442
443
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
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
473
475 return self.column_count
476
477 @gui_function
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
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
526
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
562
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
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
616
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
628
629
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
636 model_updated = False
637 try:
638 setattr( o, attribute, new_value )
639
640
641
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
649 pass
650
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
658 try:
659 self.admin.flush( o )
660 except DatabaseError, e:
661
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
669
670
671 if model_updated and hasattr(o, 'id') and o.id:
672
673
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
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
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
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
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
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
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
765
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
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
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
800 self.collection_getter().remove( o )
801 self._rows -= 1
802
803 @model_function
805 self.collection_getter().append( o )
806 self._rows += 1
807
808 @model_function
829
830 @gui_function
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
845
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
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
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
887
888
889
890
891
892
893
894
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
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
918 """Get the admin object associated with this model"""
919 return self.admin
920