| 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 |
|
|---|
| 30 | import logging
|
|---|
| 31 | logger = logging.getLogger( 'camelot.view.controls.formview' )
|
|---|
| 32 |
|
|---|
| 33 | from PyQt4 import QtCore
|
|---|
| 34 | from PyQt4.QtCore import Qt
|
|---|
| 35 | from PyQt4 import QtGui
|
|---|
| 36 | from camelot.view.model_thread import model_function, post
|
|---|
| 37 | from camelot.view.controls.view import AbstractView
|
|---|
| 38 |
|
|---|
| 39 | class 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 |
|
|---|
| 171 | class 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 )
|
|---|