root/trunk/camelot/view/proxy/collection_proxy.py @ 1174

Revision 1174, 37.8 KB (checked in by erikj, 6 months ago)

implement open form, delete row from sorted collections

Line 
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
30The proxy represents them in the gui thread and provides access to the data
31with zero delay.  If the data is not yet present in the proxy, dummy data is
32returned and an update signal is emitted when the correct data is available.
33"""
34
35import logging
36logger = logging.getLogger( 'camelot.view.proxy.collection_proxy' )
37
38import elixir
39import datetime
40from PyQt4.QtCore import Qt
41from PyQt4 import QtGui, QtCore
42import sip
43
44from sqlalchemy.orm.session import Session
45from camelot.view.art import Icon
46from camelot.view.fifo import fifo
47from camelot.view.controls import delegates
48from camelot.view.remote_signals import get_signal_handler
49from camelot.view.model_thread import gui_function, \
50                                      model_function, post
51
52
53class 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
70def 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
84def 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
99def 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
127def 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 
165from camelot.view.proxy import ValueLoading
166
167class EmptyRowData( object ):
168    def __getitem__( self, column ):
169        return ValueLoading
170        return None
171   
172empty_row_data = EmptyRowData()
173
174class 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   
184class 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     
Note: See TracBrowser for help on using the browser.