# ============================================================================
#
# Copyright (C) 2007-2013 Conceptive Engineering bvba. All rights reserved.
# www.conceptive.be / info@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 info@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
# info@conceptive.be
#
# ============================================================================
import functools
from PyQt4 import QtGui, QtCore
from camelot.admin.action import ActionStep
from camelot.admin.action.form_action import FormActionGuiContext
from camelot.core.exception import CancelRequest
from camelot.core.utils import ugettext_lazy as _
from camelot.core.utils import ugettext
from camelot.view.action_runner import hide_progress_dialog
from camelot.view.art import Icon
from camelot.view.controls import delegates
from camelot.view.controls.actionsbox import ActionsBox
from camelot.view.controls.standalone_wizard_page import StandaloneWizardPage
from camelot.view.model_thread import post
from camelot.view.proxy import ValueLoading
[docs]class ChangeObjectDialog( StandaloneWizardPage ):
"""A dialog to change an object. This differs from a FormView in that
it does not contains Actions, and has an OK button that is enabled when
the object is valid.
:param obj: The object to change
:param admin: The admin class used to create a form
.. image:: /_static/actionsteps/change_object.png
"""
def __init__( self,
obj,
admin,
title = _('Please complete'),
subtitle = _('Complete the form and press the OK button'),
icon = Icon('tango/22x22/categories/preferences-system.png'),
parent=None,
flags=QtCore.Qt.Dialog ):
from camelot.view.controls.formview import FormWidget
from camelot.view.proxy.collection_proxy import CollectionProxy
super(ChangeObjectDialog, self).__init__( '', parent, flags )
self.setWindowTitle( admin.get_verbose_name() )
self.set_banner_logo_pixmap( icon.getQPixmap() )
self.set_banner_title( unicode(title) )
self.set_banner_subtitle( unicode(subtitle) )
self.banner_widget().setStyleSheet('background-color: white;')
model = CollectionProxy(admin, lambda:[obj], admin.get_fields)
validator = model.get_validator()
layout = QtGui.QHBoxLayout()
layout.setObjectName( 'form_and_actions_layout' )
form_widget = FormWidget( parent=self, admin=admin )
layout.addWidget( form_widget )
validator.validity_changed_signal.connect( self._validity_changed )
form_widget.set_model( model )
form_widget.setObjectName( 'form' )
if hasattr(admin, 'form_size') and admin.form_size:
form_widget.setMinimumSize(admin.form_size[0], admin.form_size[1])
self.main_widget().setLayout(layout)
self.gui_context = FormActionGuiContext()
self.gui_context.workspace = self
self.gui_context.admin = admin
self.gui_context.view = self
self.gui_context.widget_mapper = self.findChild( QtGui.QDataWidgetMapper,
'widget_mapper' )
cancel_button = QtGui.QPushButton( ugettext('Cancel') )
cancel_button.setObjectName( 'cancel' )
ok_button = QtGui.QPushButton( ugettext('OK') )
ok_button.setObjectName( 'ok' )
ok_button.setEnabled( False )
layout = QtGui.QHBoxLayout()
layout.setDirection( QtGui.QBoxLayout.RightToLeft )
layout.addWidget( ok_button )
layout.addWidget( cancel_button )
layout.addStretch()
self.buttons_widget().setLayout( layout )
cancel_button.pressed.connect( self.reject )
ok_button.pressed.connect( self.accept )
admin._apply_form_state( self )
# do inital validation, so the validity changed signal is valid
self._validity_changed( 0 )
# set the actions in the actions panel
get_actions = admin.get_form_actions
post( functools.update_wrapper( functools.partial( get_actions,
None ),
get_actions ),
self.set_actions )
@QtCore.pyqtSlot(list)
def set_actions(self, actions):
layout = self.findChild(QtGui.QLayout, 'form_and_actions_layout' )
if actions and layout:
side_panel_layout = QtGui.QVBoxLayout()
actions_widget = ActionsBox( parent = self,
gui_context = self.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 )
@QtCore.pyqtSlot(int)
def _validity_changed(self, row):
form = self.findChild( QtGui.QWidget, 'form' )
if not form:
return
model = form.get_model()
def is_valid():
return model.get_validator().isValid(0)
post(is_valid, self._change_complete)
def _change_complete(self, complete):
ok_button = self.findChild( QtGui.QPushButton, 'ok' )
cancel_button = self.findChild( QtGui.QPushButton, 'cancel' )
if ok_button != None:
ok_button.setEnabled( complete )
ok_button.setDefault( complete )
if cancel_button != None:
ok_button.setDefault( not complete )
[docs]class ChangeObjectsDialog( StandaloneWizardPage ):
"""A dialog to change a list of objects. This differs from a ListView in
that it does not contains Actions, and has an OK button that is enabled when
all objects are valid.
:param objects: The object to change
:param admin: The admin class used to create a form
.. image:: /_static/actionsteps/change_object.png
"""
def __init__( self,
objects,
admin,
parent = None,
flags = QtCore.Qt.Window ):
from camelot.view.controls import editors
from camelot.view.proxy.collection_proxy import CollectionProxy
super(ChangeObjectsDialog, self).__init__( '', parent, flags )
self.banner_widget().setStyleSheet('background-color: white;')
model = CollectionProxy( admin, lambda:objects, admin.get_columns)
self.validator = model.get_validator()
self.validator.validity_changed_signal.connect( self.update_complete )
model.layoutChanged.connect( self.validate_all_rows )
table_widget = editors.One2ManyEditor(
admin = admin,
parent = self,
create_inline = True,
)
table_widget.set_value( model )
table_widget.setObjectName( 'table_widget' )
note = editors.NoteEditor( parent=self )
note.set_value(None)
note.setObjectName( 'note' )
layout = QtGui.QVBoxLayout()
layout.addWidget( table_widget )
layout.addWidget( note )
self.main_widget().setLayout( layout )
self.set_default_buttons()
ok_button = self.buttons_widget().findChild( QtGui.QPushButton, 'accept' )
ok_button.setEnabled( False )
self.validate_all_rows()
@QtCore.pyqtSlot()
def validate_all_rows(self):
post( self.validator.validate_all_rows, self._all_rows_validated)
def _all_rows_validated(self, *args):
self.update_complete( 0 )
@QtCore.pyqtSlot(int)
def update_complete(self, row=0):
complete = (self.validator.number_of_invalid_rows()==0)
note = self.findChild( QtGui.QWidget, 'note' )
ok = self.findChild( QtGui.QWidget, 'accept' )
if note != None and ok != None:
ok.setEnabled( complete )
if complete:
note.set_value( None )
else:
note.set_value(_(
'Please correct the data above before proceeding with the '
'import.<br/>Incorrect cells have a pink background.'
))
[docs]class ChangeObject( ActionStep ):
"""
Pop up a form for the user to change an object
:param obj: the object to change
:param admin: an instance of an admin class to use to edit the
object, None if the default is to be taken
"""
def __init__( self, obj, admin=None ):
self._obj = obj
self._admin = admin
[docs] def get_object( self ):
"""Use this method to get access to the object to change in unit tests
:return: the object to change
"""
return self._obj
[docs] def render( self, gui_context ):
"""create the dialog. this method is used to unit test
the action step."""
cls = self._obj.__class__
admin = self._admin or gui_context.admin.get_related_admin( cls )
dialog = ChangeObjectDialog( self._obj, admin )
return dialog
def gui_run( self, gui_context ):
dialog = self.render( gui_context )
with hide_progress_dialog( gui_context ):
result = dialog.exec_()
if result == QtGui.QDialog.Rejected:
raise CancelRequest()
return self._obj
[docs]class ChangeObjects( ActionStep ):
"""
Pop up a list for the user to change objects
:param objects: a list of objects to change
:param admin: an instance of an admin class to use to edit the objects.
.. image:: /_static/listactions/import_from_file_preview.png
This action step can be customised using these attributes :
.. attribute:: window_title
the window title of the dialog shown
.. attribute:: title
the title of the dialog shown
.. attribute:: subtitle
the subtitle of the dialog shown
.. attribute:: icon
the :class:`camelot.view.art.Icon` in the top right corner of
the dialog
"""
def __init__( self, objects, admin ):
self.objects = objects
self.admin = admin
self.window_title = admin.get_verbose_name_plural()
self.title = _('Data Preview')
self.subtitle = _('Please review the data below.')
self.icon = Icon('tango/32x32/mimetypes/x-office-spreadsheet.png')
[docs] def get_objects( self ):
"""Use this method to get access to the objects to change in unit tests
:return: the object to change
"""
return self.objects
[docs] def render( self ):
"""create the dialog. this method is used to unit test
the action step."""
dialog = ChangeObjectsDialog( self.objects,
self.admin )
dialog.setWindowTitle( unicode( self.window_title ) )
dialog.set_banner_title( unicode( self.title ) )
dialog.set_banner_subtitle( unicode( self.subtitle ) )
dialog.set_banner_logo_pixmap( self.icon.getQPixmap() )
#
# the dialog cannot estimate its size, so use 75% of screen estate
#
desktop = QtGui.QApplication.desktop()
available_geometry = desktop.availableGeometry( dialog )
dialog.resize( available_geometry.width() * 0.75,
available_geometry.height() * 0.75 )
return dialog
def gui_run( self, gui_context ):
dialog = self.render()
with hide_progress_dialog( gui_context ):
result = dialog.exec_()
if result == QtGui.QDialog.Rejected:
raise CancelRequest()
return self.objects
[docs]class ChangeFieldDialog( StandaloneWizardPage ):
"""A dialog to change a field of an object.
"""
def __init__( self,
admin,
field_attributes,
parent = None,
flags=QtCore.Qt.Dialog ):
super(ChangeFieldDialog, self).__init__( '', parent, flags )
from camelot.view.controls.editors import ChoicesEditor
self.field_attributes = field_attributes
self.field = None
self.value = None
self.setWindowTitle( admin.get_verbose_name_plural() )
self.set_banner_title( _('Replace field contents') )
self.set_banner_subtitle( _('Select the field to update and enter its new value') )
self.banner_widget().setStyleSheet('background-color: white;')
editor = ChoicesEditor( parent=self )
editor.setObjectName( 'field_choice' )
layout = QtGui.QVBoxLayout()
layout.addWidget( editor )
self.main_widget().setLayout( layout )
def filter(attributes):
if not attributes['editable']:
return False
if attributes['delegate'] in (delegates.One2ManyDelegate,):
return False
return True
choices = [(field, unicode(attributes['name'])) for field, attributes in field_attributes.items() if filter(attributes)]
choices.sort( key = lambda choice:choice[1] )
editor.set_choices( choices + [(None,'')] )
editor.set_value( None )
self.field_changed( 0 )
editor.currentIndexChanged.connect( self.field_changed )
self.set_default_buttons()
@QtCore.pyqtSlot(int)
def field_changed(self, index):
import sqlalchemy.schema
selected_field = ValueLoading
editor = self.findChild( QtGui.QWidget, 'field_choice' )
value_editor = self.findChild( QtGui.QWidget, 'value_editor' )
if editor != None:
selected_field = editor.get_value()
if value_editor != None:
value_editor.deleteLater()
if selected_field not in (None, ValueLoading):
self.field = selected_field
self.value = None
field_attributes = self.field_attributes[selected_field]
static_field_attributes = dict( (k,v) for k,v in field_attributes.items() if not callable(v) )
delegate = field_attributes['delegate']( parent = self,
**static_field_attributes)
option = QtGui.QStyleOptionViewItem()
option.version = 5
value_editor = delegate.createEditor( self, option, None )
value_editor.setObjectName( 'value_editor' )
value_editor.set_field_attributes( **static_field_attributes )
self.main_widget().layout().addWidget( value_editor )
value_editor.editingFinished.connect( self.value_changed )
# try to set sensible defaults for value
if isinstance(delegate, delegates.Many2OneDelegate):
value_editor.set_value(lambda:None)
else:
default = static_field_attributes.get('default', None)
choices = static_field_attributes.get('choices', None)
if default != None and not isinstance(default, sqlalchemy.schema.ColumnDefault):
value_editor.set_value( default )
elif choices and len(choices):
value_editor.set_value( choices[0][0] )
else:
value_editor.set_value( None )
# force the value editor, since the previous one is still around
self.value_changed( value_editor )
def value_changed(self, value_editor=None):
if not value_editor:
value_editor = self.findChild( QtGui.QWidget, 'value_editor' )
if value_editor != None:
delegate = self.field_attributes[self.field]['delegate']
value = value_editor.get_value()
# make sure a value is always callable
if issubclass(delegate, delegates.Many2OneDelegate):
value_getter = value
else:
value_getter = lambda:value
self.value = value_getter
[docs]class ChangeField( ActionStep ):
"""
Pop up a list of fields from an object a user can change. When the
user selects a field, an appropriate widget is shown to change the
value of that field.
:param admin: the admin of the object of which to change the field
:param field_attributes: a list of field attributes of the fields that
can be changed. If `None` is given, all fields are shown.
This action step returns a tuple with the name of the selected field, and
its new value.
"""
def __init__(self, admin, field_attributes = None ):
super( ChangeField, self ).__init__()
self.admin = admin
if field_attributes == None:
field_attributes = admin.get_all_fields_and_attributes()
self.field_attributes = field_attributes
[docs] def render( self ):
"""create the dialog. this method is used to unit test
the action step."""
return ChangeFieldDialog( self.admin,
self.field_attributes )
def gui_run( self, gui_context ):
dialog = self.render()
with hide_progress_dialog( gui_context ):
result = dialog.exec_()
if result == QtGui.QDialog.Rejected:
raise CancelRequest()
return (dialog.field, dialog.value)