root/trunk/camelot/view/controls/formview.py @ 1186

Revision 1186, 12.6 KB (checked in by erikj, 6 months ago)

prepare expanded search and sorting of query proxy models

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"""form view"""
29
30import logging
31logger = logging.getLogger( 'camelot.view.controls.formview' )
32
33from PyQt4 import QtCore
34from PyQt4.QtCore import Qt
35from PyQt4 import QtGui
36from camelot.view.model_thread import model_function, post
37from camelot.view.controls.view import AbstractView
38
39class FormWidget( QtGui.QWidget ):
40   
41    changed_signal = QtCore.SIGNAL( 'changed()' )
42   
43    def __init__(self, admin):
44        QtGui.QWidget.__init__(self)
45        self._admin = admin
46        self._widget_mapper = QtGui.QDataWidgetMapper()
47        self._widget_layout = QtGui.QHBoxLayout()
48        self._widget_layout.setSpacing( 0 )
49        self._widget_layout.setMargin( 0 )
50        self._index = 0
51        self._model = None
52        self._form = None
53        self._columns = None
54        self._delegate = None
55        self.setLayout( self._widget_layout )
56               
57    def get_model(self):
58        return self._model
59   
60    def set_model(self, model):
61        self._model = model
62        sig = 'dataChanged(const QModelIndex &, const QModelIndex &)'
63        self.connect( self._model, QtCore.SIGNAL( sig ), self._data_changed )
64        self.connect( self._model, QtCore.SIGNAL( 'layoutChanged()' ), self._layout_changed )
65        self.connect( self._model, self._model.item_delegate_changed_signal, self._item_delegate_changed )
66        self._widget_mapper.setModel( model )
67       
68        def get_columns_and_form():
69            return ( self._model.getColumns(), self._admin.get_form_display() )
70     
71        post( get_columns_and_form, self._set_columns_and_form )
72       
73    def clear_mapping(self):
74        self._widget_mapper.clearMapping()
75         
76    def _data_changed( self, index_from, index_to ):
77        #@TODO: only revert if this form is in the changed range
78        self._widget_mapper.revert()
79        self.emit(self.changed_signal)
80       
81    def _layout_changed(self):
82        self._widget_mapper.revert()
83        self.emit(self.changed_signal)
84       
85    def _item_delegate_changed(self):
86        from camelot.view.controls.delegates.delegatemanager import DelegateManager
87        self._delegate = self._model.getItemDelegate()
88        assert self._delegate
89        assert isinstance(self._delegate, DelegateManager)
90        self._create_widgets()
91                     
92    def set_index(self, index):
93        self._index = index
94        self._widget_mapper.setCurrentIndex( self._index )
95       
96    def get_index(self):
97        return self._widget_mapper.currentIndex()
98       
99    def submit(self):
100        self._widget_mapper.submit()
101               
102    def to_first(self):
103        self._widget_mapper.toFirst()
104       
105    def to_last(self):
106        self._widget_mapper.toLast()
107       
108    def to_next(self):
109        self._widget_mapper.toNext()
110       
111    def to_previous(self):
112        self._widget_mapper.toPrevious()
113       
114    def _set_columns_and_form(self, columns_and_form ):
115        self._columns, self._form = columns_and_form
116        self._create_widgets()
117       
118    def _create_widgets( self ):
119        """Create value and label widgets"""
120        from camelot.view.controls.user_translatable_label import UserTranslatableLabel
121        from camelot.view.controls.editors.wideeditor import WideEditor
122        #
123        # Dirty trick to make form views work during unit tests, since unit tests
124        # have no event loop running, so the delegate will never be set, so we get
125        # it and are sure it will be there if we are running without threads
126        #
127        if not self._delegate:
128            self._delegate = self._model.getItemDelegate()
129        #
130        # end of dirty trick
131        #
132        # only if all information is available, we can start building the form
133        if not (self._form and self._columns and self._delegate):
134            return
135        widgets = {}
136        self._widget_mapper.setItemDelegate( self._delegate )
137        option = QtGui.QStyleOptionViewItem()
138        # set version to 5 to indicate the widget will appear on a
139        # a form view and not on a table view
140        option.version = 5
141   
142        for i, ( field_name, field_attributes ) in enumerate( self._columns ):
143            model_index = self._model.index( self._index, i )
144            hide_title = False
145            if 'hide_title' in field_attributes:
146                hide_title = field_attributes['hide_title']
147            widget_label = None
148            widget_editor = self._delegate.createEditor( self, option, model_index )
149            if not hide_title:
150                widget_label = UserTranslatableLabel( field_attributes['name'] )
151                if not isinstance(widget_editor, WideEditor):
152                    widget_label.setAlignment( Qt.AlignVCenter | Qt.AlignRight )
153     
154            # required fields font is bold
155            if ( 'nullable' in field_attributes ) and \
156               ( not field_attributes['nullable'] ):
157                font = QtGui.QApplication.font()
158                font.setBold( True )
159                widget_label.setFont( font )
160       
161            assert widget_editor
162            assert isinstance(widget_editor, QtGui.QWidget)
163           
164            self._widget_mapper.addMapping( widget_editor, i )
165            widgets[field_name] = ( widget_label, widget_editor )
166   
167        self._widget_mapper.setCurrentIndex( self._index )
168        self._widget_layout.insertWidget( 0, self._form.render( widgets, self ) )
169        self._widget_layout.setContentsMargins( 7, 7, 7, 7 )
170       
171class FormView( AbstractView ):
172    """A FormView is the combination of a FormWidget, possible actions and menu items
173   
174.. form_widget: The class to be used as a the form widget inside the form view
175
176    """
177
178    form_widget = FormWidget
179   
180    def __init__( self, title, admin, model, index ):
181        AbstractView.__init__( self )
182        layout = QtGui.QVBoxLayout()
183        self._form = FormWidget(admin)
184        self.model = model
185        self.title_prefix = title
186        self.admin = admin
187        self.connect(self._form, FormWidget.changed_signal, self.update_title)
188        self._form.set_model(model)
189        self._form.set_index(index)
190        layout.addWidget(self._form)
191        self.change_title(title)
192        self.closeAfterValidation = QtCore.SIGNAL( 'closeAfterValidation()' )
193        self.setLayout( layout )
194   
195        if hasattr( admin, 'form_size' ) and admin.form_size:
196            self.setMinimumSize( admin.form_size[0], admin.form_size[1] )
197     
198        self.validator = admin.create_validator( model )
199        self.validate_before_close = True
200
201        def getActions():
202            return admin.get_form_actions( None )
203     
204        post( getActions, self.setActions )
205        self.update_title()
206   
207    def update_title( self ):
208 
209        def get_title():
210            obj = self.getEntity()
211            return u'%s %s' % ( self.title_prefix, self.admin.get_verbose_identifier( obj ) )
212     
213        post( get_title, self.change_title )
214   
215    def getEntity( self ):
216        return self.model._get_object( self._form.get_index() )
217   
218    def setActions( self, actions ):
219        if actions:
220            side_panel_layout = QtGui.QVBoxLayout()
221            from camelot.view.controls.actionsbox import ActionsBox
222            logger.debug( 'setting Actions for formview' )
223            self.actions_widget = ActionsBox( self, self.getEntity )
224            self.actions_widget.setActions( actions )
225            side_panel_layout.insertWidget( 1, self.actions_widget )
226            side_panel_layout.addStretch()
227            self.widget_layout.addLayout(side_panel_layout)
228     
229    def viewFirst( self ):
230        """select model's first row"""
231        self._form.submit()
232        self._form.to_first()
233        self.update_title()
234   
235    def viewLast( self ):
236        """select model's last row"""
237        # submit should not happen a second time, since then we don't want
238        # the widgets data to be written to the model
239        self._form.submit()
240        self._form.to_last()
241        self.update_title()
242   
243    def viewNext( self ):
244        """select model's next row"""
245        # submit should not happen a second time, since then we don't want
246        # the widgets data to be written to the model
247        self._form.submit()
248        self._form.to_next()
249        self.update_title()
250   
251    def viewPrevious( self ):
252        """select model's previous row"""
253        # submit should not happen a second time, since then we don't want
254        # the widgets data to be written to the model
255        self._form.submit()
256        self._form.to_previous()
257        self.update_title()
258   
259    def showMessage( self, valid ):
260        import sip
261        if not valid:
262            reply = self.validator.validityDialog( self._form.get_index(), self ).exec_()
263            if reply == QtGui.QMessageBox.Discard:
264            # clear mapping to prevent data being written again to the model,
265            # then we reverted the row
266                self._form.clear_mapping()
267                self.model.revertRow( self._form.get_index() )
268                self.validate_before_close = False
269                self.emit( self.closeAfterValidation )
270        else:
271            self.validate_before_close = False
272            if not sip.isdeleted( self ):
273                self.emit( self.closeAfterValidation )
274                   
275    def validateClose( self ):
276        logger.debug( 'validate before close : %s' % self.validate_before_close )
277        if self.validate_before_close:
278            # submit should not happen a second time, since then we don't
279            # want the widgets data to be written to the model
280            self._form.submit()
281     
282            def validate():
283                return self.validator.isValid( self._form.get_index() )
284       
285            post( validate, self.showMessage )
286            return False
287     
288        return True
289   
290    def closeEvent( self, event ):
291        logger.debug( 'formview closed' )
292        if self.validateClose():
293            event.accept()
294        else:
295            event.ignore()
296     
297    @model_function
298    def toHtml( self ):
299        """generates html of the form"""
300        from jinja import Environment
301   
302        def to_html( d = u'' ):
303            """Jinja 1 filter to convert field values to their default html
304            representation
305            """
306     
307            def wrapped_in_table( env, context, value ):
308                if isinstance( value, list ):
309                    return u'<table><tr><td>' + \
310                           u'</td></tr><tr><td>'.join( [unicode( e ) for e in value] ) + \
311                           u'</td></tr></table>'
312                return unicode( value )
313       
314            return wrapped_in_table
315     
316        entity = self.getEntity()
317        fields = self.admin.get_fields()
318        table = [dict( field_attributes = field_attributes,
319                      value = getattr( entity, name ) )
320                      for name, field_attributes in fields]
321   
322        context = {
323          'title': self.admin.get_verbose_name(),
324          'table': table,
325        }
326   
327        from camelot.view.templates import loader
328        env = Environment( loader = loader )
329        env.filters['to_html'] = to_html
330        tp = env.get_template( 'form_view.html' )
331   
332        return tp.render( context )
Note: See TracBrowser for help on using the browser.