# ============================================================================
#
# Copyright (C) 2007-2011 Conceptive Engineering bvba. All rights reserved.
# www.conceptive.be / project-camelot@conceptive.be
#
# This file is part of the Camelot Library.
#
# This file may be used under the terms of the GNU General Public
# License version 2.0 as published by the Free Software Foundation
# and appearing in the file license.txt included in the packaging of
# this file. Please review this information to ensure GNU
# General Public Licensing requirements will be met.
#
# If you are unsure which license is appropriate for your use, please
# visit www.python-camelot.com or contact project-camelot@conceptive.be
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#
# For use of this library in commercial applications, please contact
# project-camelot@conceptive.be
#
# ============================================================================
"""form view"""
import functools
import logging
LOGGER = logging.getLogger('camelot.view.controls.formview')
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from camelot.admin.action.form_action import FormActionGuiContext
from camelot.view.art import Icon
from camelot.view.model_thread import post
from camelot.view.model_thread import model_function
from camelot.view.controls.view import AbstractView
from camelot.view.controls.statusbar import StatusBar
from camelot.view import register
from camelot.view.action import ActionFactory
[docs]class ContextMenuAction(QtGui.QAction):
default_icon = Icon('tango/16x16/categories/applications-system.png')
def __init__(self, parent, title, icon = None):
"""
:param parent: the widget on which the context menu will be placed
:param title: text displayed in the context menu
:param icon: camelot.view.art.Icon object
"""
super(ContextMenuAction, self).__init__(title, parent)
self.icon = icon
if self.icon:
self.setIcon(self.icon.getQIcon())
else:
self.setIcon(self.default_icon.getQIcon())
[docs]class FormEditors( object ):
"""A class that holds the editors used on a form
"""
option = None
bold_font = None
def __init__( self, columns, widget_mapper, delegate, admin ):
if self.option == None:
self.option = QtGui.QStyleOptionViewItem()
# set version to 5 to indicate the widget will appear on a
# a form view and not on a table view
self.option.version = 5
self.bold_font = QtGui.QApplication.font()
self.bold_font.setBold(True)
self._admin = admin
self._widget_mapper = widget_mapper
self._field_attributes = dict()
self._index = dict()
for i, (field_name, field_attributes ) in enumerate( columns):
self._field_attributes[field_name] = field_attributes
self._index[field_name] = i
[docs] def create_editor( self, field_name, parent ):
"""
:return: a :class:`QtGuiQWidget` or None if field_name is unknown
"""
index = self._index[field_name]
model = self._widget_mapper.model()
delegate = self._widget_mapper.itemDelegate()
model_index = model.index( self._widget_mapper.currentIndex(), index )
widget_editor = delegate.createEditor(
parent,
self.option,
model_index
)
widget_editor.setObjectName('%s_editor'%field_name)
delegate.setEditorData( widget_editor, model_index )
self._widget_mapper.addMapping( widget_editor, index )
return widget_editor
[docs] def create_label( self, field_name, editor, parent ):
from camelot.view.controls.field_label import FieldLabel
from camelot.view.controls.editors.wideeditor import WideEditor
field_attributes = self._field_attributes[field_name]
hide_title = field_attributes.get( 'hide_title', False )
widget_label = None
if not hide_title:
widget_label = FieldLabel(
field_name,
field_attributes['name'],
field_attributes,
self._admin
)
widget_label.setObjectName('%s_label'%field_name)
if not isinstance(editor, WideEditor):
widget_label.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
# required fields font is bold
nullable = field_attributes.get( 'nullable', True )
if not nullable:
widget_label.setFont( self.bold_font )
return widget_label
[docs]class FormWidget(QtGui.QWidget):
"""A form widget comes inside a form view or inside an embedded manytoone editor"""
changed_signal = QtCore.pyqtSignal( int )
def __init__(self, parent, admin):
QtGui.QWidget.__init__(self, parent)
self._admin = admin
widget_mapper = QtGui.QDataWidgetMapper(self)
widget_mapper.setObjectName('widget_mapper')
if self._admin.get_save_mode()=='on_leave':
widget_mapper.setSubmitPolicy(QtGui.QDataWidgetMapper.ManualSubmit)
widget_layout = QtGui.QHBoxLayout()
widget_layout.setSpacing(0)
widget_layout.setContentsMargins(0, 0, 0, 0)
self._index = 0
self._model = None
self._form = None
self._columns = None
self._delegate = None
self.setLayout(widget_layout)
[docs] def set_model(self, model):
self._model = model
self._model.dataChanged.connect( self._data_changed )
self._model.layoutChanged.connect( self._layout_changed )
self._model.item_delegate_changed_signal.connect( self._item_delegate_changed )
self._model.setObjectName( 'model' )
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.setModel( model )
register.register( model, widget_mapper )
def get_columns_and_form():
return (self._model.getColumns(), self._admin.get_form_display())
post(get_columns_and_form, self._set_columns_and_form)
[docs] def clear_mapping(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.clearMapping()
@QtCore.pyqtSlot( QtCore.QModelIndex, QtCore.QModelIndex )
def _data_changed(self, index_from, index_to):
#@TODO: only revert if this form is in the changed range
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.revert()
self.changed_signal.emit( widget_mapper.currentIndex() )
@QtCore.pyqtSlot()
def _layout_changed(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.revert()
self.changed_signal.emit( widget_mapper.currentIndex() )
@QtCore.pyqtSlot()
def _item_delegate_changed(self):
from camelot.view.controls.delegates.delegatemanager import \
DelegateManager
self._delegate = self._model.getItemDelegate()
self._delegate.setObjectName('delegate')
assert self._delegate != None
assert isinstance(self._delegate, DelegateManager)
self._create_widgets()
[docs] def set_index(self, index):
self._index = index
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.setCurrentIndex(self._index)
[docs] def get_index(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
return widget_mapper.currentIndex()
[docs] def submit(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.submit()
[docs] def to_first(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.toFirst()
self.changed_signal.emit( widget_mapper.currentIndex() )
[docs] def to_last(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.toLast()
self.changed_signal.emit( widget_mapper.currentIndex() )
[docs] def to_next(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.toNext()
self.changed_signal.emit( widget_mapper.currentIndex() )
[docs] def to_previous(self):
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if widget_mapper:
widget_mapper.toPrevious()
self.changed_signal.emit( widget_mapper.currentIndex() )
[docs] def export_ooxml(self):
from camelot.view.export.word import open_stream_in_word
def create_ooxml_export(row):
# print self._columns
def ooxml_export():
# TODO insert delegates
fields = self._admin.get_all_fields_and_attributes()
delegates = {}
for field_name, attributes in fields.items():
delegates[field_name] = attributes['delegate'](**attributes)
obj = self._model._get_object(row)
document = self._form.render_ooxml(obj, delegates)
open_stream_in_word( document )
return ooxml_export
post(create_ooxml_export(self.get_index()))
@QtCore.pyqtSlot(tuple)
def _set_columns_and_form(self, columns_and_form ):
self._columns, self._form = columns_and_form
self._create_widgets()
def _create_widgets(self):
"""Create value and label widgets"""
#
# Dirty trick to make form views work during unit tests, since unit
# tests have no event loop running, so the delegate will never be set,
# so we get it and are sure it will be there if we are running without
# threads
#
if not self._delegate:
self._delegate = self._model.getItemDelegate()
#
# end of dirty trick
#
# only if all information is available, we can start building the form
if not (self._form and self._columns and self._delegate):
return
widgets = {}
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
if not widget_mapper:
return
LOGGER.debug( 'begin creating widgets' )
widget_mapper.setItemDelegate(self._delegate)
widgets = FormEditors( self._columns, widget_mapper, self._delegate, self._admin )
widget_mapper.setCurrentIndex( self._index )
LOGGER.debug( 'put widgets on form' )
self.layout().insertWidget(0, self._form.render( widgets, self, True) )
LOGGER.debug( 'done' )
#self._widget_layout.setContentsMargins(7, 7, 7, 7)
[docs]class FormView(AbstractView):
"""A FormView is the combination of a FormWidget, possible actions and menu
items
.. form_widget: The class to be used as a the form widget inside the form
view"""
form_widget = FormWidget
def __init__(self, title, admin, model, index):
AbstractView.__init__(self)
layout = QtGui.QVBoxLayout()
form_and_actions_layout = QtGui.QHBoxLayout()
form_and_actions_layout.setObjectName('form_and_actions_layout')
layout.addLayout( form_and_actions_layout )
self.model = model
self.admin = admin
self.title_prefix = title
form = FormWidget(self, admin)
form.setObjectName( 'form' )
form.changed_signal.connect( self.update_title )
form.set_model(model)
form.set_index(index)
form_and_actions_layout.addWidget(form)
statusbar = StatusBar(self)
statusbar.setObjectName('statusbar')
statusbar.setSizeGripEnabled(False)
layout.addWidget(statusbar)
layout.setAlignment(statusbar, Qt.AlignBottom)
self.setLayout(layout)
self.change_title(title)
if hasattr(admin, 'form_size') and admin.form_size:
self.setMinimumSize(admin.form_size[0], admin.form_size[1])
self.validator = admin.create_validator(model)
self.validate_before_close = True
def get_actions():
return admin.get_form_actions(None)
post(get_actions, self.setActions)
#
# Define actions
#
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.addAction( ActionFactory.view_first(self, self.viewFirst) )
self.addAction( ActionFactory.view_last(self, self.viewLast) )
self.addAction( ActionFactory.view_next(self, self.viewNext) )
self.addAction( ActionFactory.view_previous(self, self.viewPrevious) )
self.addAction( ActionFactory.refresh(self, self.refresh_session) )
# Disabled the export to Word feature until it's finish
# self.addAction( ActionFactory.export_ooxml(self, form.export_ooxml) )
@QtCore.pyqtSlot()
[docs] def refresh_session(self):
from elixir import session
from camelot.core.orm import refresh_session
post( functools.update_wrapper( functools.partial( refresh_session, session ), refresh_session ) )
@QtCore.pyqtSlot()
def _get_title( self, index ):
obj = self.model._get_object( index )
return u'%s %s' % (
self.title_prefix,
self.admin.get_verbose_identifier(obj)
)
@QtCore.pyqtSlot( int )
[docs] def update_title(self, current_index ):
post( self._get_title, self.change_title, args=(current_index,) )
#def getEntity(self):
#form = self.findChild(QtGui.QWidget, 'form' )
#if form:
#return self.model._get_object(form.get_index())
@QtCore.pyqtSlot(list)
[docs] def setActions(self, actions):
form = self.findChild(QtGui.QWidget, 'form' )
layout = self.findChild(QtGui.QLayout, 'form_and_actions_layout' )
if actions and form and layout:
side_panel_layout = QtGui.QVBoxLayout()
widget_mapper = self.findChild(QtGui.QDataWidgetMapper, 'widget_mapper' )
from camelot.view.controls.actionsbox import ActionsBox
LOGGER.debug('setting Actions for formview')
gui_context = FormActionGuiContext()
gui_context.workspace = self
gui_context.admin = self.admin
gui_context.widget_mapper = widget_mapper
actions_widget = ActionsBox( parent=self, gui_context=gui_context )
actions_widget.setObjectName('actions')
actions_widget.set_actions( actions )
side_panel_layout.addWidget( actions_widget )
side_panel_layout.addStretch()
layout.addLayout( side_panel_layout )
[docs] def viewFirst(self):
"""select model's first row"""
form = self.findChild(QtGui.QWidget, 'form' )
if form:
form.submit()
form.to_first()
[docs] def viewLast(self):
"""select model's last row"""
# submit should not happen a second time, since then we don't want
# the widgets data to be written to the model
form = self.findChild(QtGui.QWidget, 'form' )
if form:
form.submit()
form.to_last()
[docs] def viewNext(self):
"""select model's next row"""
# submit should not happen a second time, since then we don't want
# the widgets data to be written to the model
form = self.findChild(QtGui.QWidget, 'form' )
if form:
form.submit()
form.to_next()
[docs] def viewPrevious(self):
"""select model's previous row"""
# submit should not happen a second time, since then we don't want
# the widgets data to be written to the model
form = self.findChild(QtGui.QWidget, 'form' )
if form:
form.submit()
form.to_previous()
@QtCore.pyqtSlot(bool)
[docs] def showMessage(self, valid):
form = self.findChild(QtGui.QWidget, 'form' )
if not valid and form:
reply = self.validator.validityDialog(
form.get_index(), self
).exec_()
if reply == QtGui.QMessageBox.Discard:
# clear mapping to prevent data being written again to the model,
# then we reverted the row
form.clear_mapping()
self.model.revertRow(form.get_index())
self.validate_before_close = False
self.close()
else:
self.validate_before_close = False
self.close()
[docs] def validateClose(self):
LOGGER.debug('validate before close : %s' % self.validate_before_close)
form = self.findChild(QtGui.QWidget, 'form' )
if self.validate_before_close and form:
# submit should not happen a second time, since then we don't
# want the widgets data to be written to the model
form.submit()
def validate():
return self.validator.isValid(form.get_index())
post(validate, self.showMessage)
return False
return True
[docs] def closeEvent(self, event):
#print 'close event'
LOGGER.debug('formview closed')
if self.validateClose():
event.accept()
else:
event.ignore()
@model_function
[docs] def toHtml(self):
"""generates html of the form"""
from jinja2 import Environment
def to_html(d = u''):
"""Jinja 1 filter to convert field values to their default html
representation
"""
def wrapped_in_table(env, context, value):
if isinstance(value, list):
return u'<table><tr><td>' + \
u'</td></tr><tr><td>'.join(
[unicode(e) for e in value]
) + u'</td></tr></table>'
return unicode(value)
return wrapped_in_table
entity = self.getEntity()
fields = self.admin.get_fields()
table = [dict( field_attributes = field_attributes,
value = getattr(entity, name ))
for name, field_attributes in fields]
context = {
'title': self.admin.get_verbose_name(),
'table': table,
}
from camelot.view.templates import loader
env = Environment(loader = loader)
env.filters['to_html'] = to_html
tp = env.get_template('form_view.html')
return tp.render(context)