Package Camelot :: Package camelot :: Package view :: Package controls :: Module formview |
[frames] | no frames]

# Source Code for Module Camelot.camelot.view.controls.formview

      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          self.emit(self.changed_signal)
    105

    106 -    def to_last(self):
    107          self._widget_mapper.toLast()
    108          self.emit(self.changed_signal)
    109

    110 -    def to_next(self):
    111          self._widget_mapper.toNext()
    112          self.emit(self.changed_signal)
    113

    114 -    def to_previous(self):
    115          self._widget_mapper.toPrevious()
    116          self.emit(self.changed_signal)
    117

    118 -    def _set_columns_and_form(self, columns_and_form ):
    119          self._columns, self._form = columns_and_form
    120          self._create_widgets()
    121

    122 -    def _create_widgets( self ):
    123          """Create value and label widgets"""
    124          from camelot.view.controls.field_label import FieldLabel
    125          from camelot.view.controls.editors.wideeditor import WideEditor
    126          #

    127          # Dirty trick to make form views work during unit tests, since unit tests

    128          # have no event loop running, so the delegate will never be set, so we get

    129          # it and are sure it will be there if we are running without threads

    130          #

    131          if not self._delegate:
    132              self._delegate = self._model.getItemDelegate()
    133          #

    134          # end of dirty trick

    135          #

    136          # only if all information is available, we can start building the form

    137          if not (self._form and self._columns and self._delegate):
    138              return
    139          widgets = {}
    140          self._widget_mapper.setItemDelegate( self._delegate )
    141          option = QtGui.QStyleOptionViewItem()
    142          # set version to 5 to indicate the widget will appear on a

    143          # a form view and not on a table view

    144          option.version = 5
    145

    146          #

    147          # this loop can take a while to complete, so processEvents is called regulary

    148          #

    149          for i, ( field_name, field_attributes ) in enumerate( self._columns ):
    150  #            if i%10==0:

    151  #                QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.ExcludeSocketNotifiers, 100)

    152              model_index = self._model.index( self._index, i )
    153              hide_title = False
    154              if 'hide_title' in field_attributes:
    155                  hide_title = field_attributes['hide_title']
    156              widget_label = None
    157              widget_editor = self._delegate.createEditor( self, option, model_index )
    158              if not hide_title:
    159                  widget_label = FieldLabel( field_name, field_attributes['name'], field_attributes, self._admin )
    160                  if not isinstance(widget_editor, WideEditor):
    161                      widget_label.setAlignment( Qt.AlignVCenter | Qt.AlignRight )
    162

    163              # required fields font is bold

    164              if ( 'nullable' in field_attributes ) and \

    165                 ( not field_attributes['nullable'] ):
    166                  font = QtGui.QApplication.font()
    167                  font.setBold( True )
    168                  widget_label.setFont( font )
    169

    170              assert widget_editor
    171              assert isinstance(widget_editor, QtGui.QWidget)
    172

    173              self._widget_mapper.addMapping( widget_editor, i )
    174              widgets[field_name] = ( widget_label, widget_editor )
    175

    176          self._widget_mapper.setCurrentIndex( self._index )
    177          self._widget_layout.insertWidget( 0, self._form.render( widgets, self ) )
    178          self._widget_layout.setContentsMargins( 7, 7, 7, 7 )
    179

    180 -class FormView( AbstractView ):
    181      """A FormView is the combination of a FormWidget, possible actions and menu items

    182

    183  .. form_widget: The class to be used as a the form widget inside the form view

    184

    185      """
    186

    187      form_widget = FormWidget
    188

    189 -    def __init__( self, title, admin, model, index ):
    190          AbstractView.__init__( self )
    191          layout = QtGui.QHBoxLayout()
    192          self._form = FormWidget(admin)
    193          self.model = model
    194          self.title_prefix = title
    195          self.admin = admin
    196          self.connect(self._form, FormWidget.changed_signal, self.update_title)
    197          self._form.set_model(model)
    198          self._form.set_index(index)
    199          layout.addWidget(self._form)
    200          self.change_title(title)
    201          self.closeAfterValidation = QtCore.SIGNAL( 'closeAfterValidation()' )
    202          self.setLayout( layout )
    203

    204          if hasattr( admin, 'form_size' ) and admin.form_size:
    205              self.setMinimumSize( admin.form_size[0], admin.form_size[1] )
    206

    207          self.validator = admin.create_validator( model )
    208          self.validate_before_close = True
    209

    210          def getActions():
    211              return admin.get_form_actions( None )
    212

    213          post( getActions, self.setActions )
    214          self.update_title()
    215

    216 -    def update_title( self ):
    217

    218          def get_title():
    219              obj = self.getEntity()
    220              return u'%s %s' % ( self.title_prefix, self.admin.get_verbose_identifier( obj ) )
    221

    222          post( get_title, self.change_title )
    223

    224 -    def getEntity( self ):
    225          return self.model._get_object( self._form.get_index() )
    226

    227 -    def setActions( self, actions ):
    228          if actions:
    229              side_panel_layout = QtGui.QVBoxLayout()
    230              from camelot.view.controls.actionsbox import ActionsBox
    231              logger.debug( 'setting Actions for formview' )
    232              self.actions_widget = ActionsBox( self, self.getEntity )
    233              action_widgets = self.actions_widget.setActions( actions )
    234              for action_widget in action_widgets:
    235                  self.connect( self._form, FormWidget.changed_signal, action_widget.changed )
    236                  action_widget.changed()
    237              side_panel_layout.insertWidget( 1, self.actions_widget )
    238              side_panel_layout.addStretch()
    239              self.layout().addLayout(side_panel_layout)
    240

    241 -    def viewFirst( self ):
    242          """select model's first row"""
    243          self._form.submit()
    244          self._form.to_first()
    245

    246 -    def viewLast( self ):
    247          """select model's last row"""
    248          # submit should not happen a second time, since then we don't want

    249          # the widgets data to be written to the model

    250          self._form.submit()
    251          self._form.to_last()
    252

    253 -    def viewNext( self ):
    254          """select model's next row"""
    255          # submit should not happen a second time, since then we don't want

    256          # the widgets data to be written to the model

    257          self._form.submit()
    258          self._form.to_next()
    259

    260 -    def viewPrevious( self ):
    261          """select model's previous row"""
    262          # submit should not happen a second time, since then we don't want

    263          # the widgets data to be written to the model

    264          self._form.submit()
    265          self._form.to_previous()
    266

    267 -    def showMessage( self, valid ):
    268          import sip
    269          if not valid:
    270              reply = self.validator.validityDialog( self._form.get_index(), self ).exec_()
    271              if reply == QtGui.QMessageBox.Discard:
    272              # clear mapping to prevent data being written again to the model,

    273              # then we reverted the row

    274                  self._form.clear_mapping()
    275                  self.model.revertRow( self._form.get_index() )
    276                  self.validate_before_close = False
    277                  self.emit( self.closeAfterValidation )
    278          else:
    279              self.validate_before_close = False
    280              if not sip.isdeleted( self ):
    281                  self.emit( self.closeAfterValidation )
    282

    283 -    def validateClose( self ):
    284          logger.debug( 'validate before close : %s' % self.validate_before_close )
    285          if self.validate_before_close:
    286              # submit should not happen a second time, since then we don't

    287              # want the widgets data to be written to the model

    288              self._form.submit()
    289

    290              def validate():
    291                  return self.validator.isValid( self._form.get_index() )
    292

    293              post( validate, self.showMessage )
    294              return False
    295

    296          return True
    297

    298 -    def closeEvent( self, event ):
    299          logger.debug( 'formview closed' )
    300          if self.validateClose():
    301              event.accept()
    302          else:
    303              event.ignore()
    304

    305      @model_function
    306 -    def toHtml( self ):
    307          """generates html of the form"""
    308          from jinja import Environment
    309

    310          def to_html( d = u'' ):
    311              """Jinja 1 filter to convert field values to their default html

    312              representation

    313              """
    314

    315              def wrapped_in_table( env, context, value ):
    316                  if isinstance( value, list ):
    317                      return u'<table><tr><td>' + \

    318                             u'</td></tr><tr><td>'.join( [unicode( e ) for e in value] ) + \

    319                             u'</td></tr></table>'
    320                  return unicode( value )
    321

    322              return wrapped_in_table
    323

    324          entity = self.getEntity()
    325          fields = self.admin.get_fields()
    326          table = [dict( field_attributes = field_attributes,

    327                        value = getattr( entity, name ) )

    328                        for name, field_attributes in fields]
    329

    330          context = {

    331            'title': self.admin.get_verbose_name(),

    332            'table': table,

    333          }
    334

    335          from camelot.view.templates import loader
    336          env = Environment( loader = loader )
    337          env.filters['to_html'] = to_html
    338          tp = env.get_template( 'form_view.html' )
    339

    340          return tp.render( context )
    341