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