# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2005-2016 Async Open Source
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public License
## as published by the Free Software Foundation; either version 2
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
""" List management for common dialogs. """
import gtk
from kiwi.enums import ListType
from kiwi.ui.objectlist import ObjectList, ObjectTree
from kiwi.ui.listdialog import ListSlave
from kiwi.utils import gsignal
from stoqlib.api import api
from stoqlib.exceptions import SelectionError, StoqlibError
from stoqlib.gui.base.dialogs import run_dialog, BasicDialog
from stoqlib.gui.base.wizards import BaseWizard
from stoqlib.gui.editors.baseeditor import BaseEditor
from stoqlib.gui.search.searchslave import SearchSlave
from stoqlib.lib.translation import stoqlib_gettext, stoqlib_ngettext
from stoqlib.lib.message import yesno
from stoqlib.lib.permissions import PermissionManager
_ = stoqlib_gettext
[docs]class ModelListSlave(ListSlave):
model_type = None
editor_class = None
columns = None
def __init__(self, parent=None, store=None, orientation=None,
reuse_store=False):
"""
Create a new ModelListDialog object.
:param store: a store connection
"""
if orientation is None:
orientation = gtk.ORIENTATION_VERTICAL
if self.columns is None:
fmt = "%s needs to set it's columns attribute"
raise TypeError(fmt % (self.__class__.__name__, ))
if self.model_type is None:
fmt = "%s needs to set it's model_type attribute"
raise TypeError(fmt % (self.__class__.__name__, ))
if not store:
store = api.get_default_store()
assert not reuse_store
self.store = store
self.parent = parent
self.reuse_store = reuse_store
columns = self.columns or self.get_columns()
ListSlave.__init__(self, columns, orientation)
self._setup_permission()
def _setup_permission(self):
if not self.editor_class:
return
pm = PermissionManager.get_permission_manager()
key = self.editor_class.model_type.__name__
if not pm.can_create(key):
self.listcontainer.set_list_type(ListType.READONLY)
elif not pm.can_edit(key) or not pm.can_delete(key):
self.listcontainer.set_list_type(ListType.ADDONLY)
else:
self.listcontainer.set_list_type(ListType.NORMAL)
def _delete_with_transaction(self, model, store):
self.delete_model(store.fetch(model), store)
def _delete_model(self, model):
if self.reuse_store:
self._delete_with_transaction(model, self.store)
else:
store = api.new_store()
self._delete_with_transaction(model, store)
store.commit(close=True)
def _prepare_run_editor(self, item):
if self.reuse_store:
self.store.savepoint('before_run_editor_list')
retval = self.run_editor(self.store, item)
if not retval:
self.store.rollback_to_savepoint('before_run_editor_list')
else:
# 1) Create a new transaction
# 2) Fetch the model from that transactions POW
# 3) Sent it to the editor and run the editor
# 4) If the return value is not None fetch it in the
# original connection (eg, self.store)
# 5) Return the value, so it can be populated by the list
store = api.new_store()
if item is not None:
model = store.fetch(item)
else:
model = None
retval = self.run_editor(store, model)
store.confirm(retval)
if retval:
retval = self.store.get(self.model_type, retval.id)
store.close()
return retval
#
# ListSlave
#
[docs] def populate(self):
return self.store.find(self.model_type)
[docs] def add_item(self):
return self._prepare_run_editor(None)
[docs] def remove_item(self, item):
if hasattr(item, 'description'):
desc = item.description
else:
desc = item.get_description()
retval = self.listcontainer.default_remove(desc)
if retval:
# Remove the list before deleting it because it'll be late
# afterwards, the object is invalid and SQLObject will complain
self.remove_list_item(item)
self._delete_model(item)
return False
[docs] def edit_item(self, item):
return bool(self._prepare_run_editor(item))
#
# Public API
#
[docs] def run_dialog(self, dialog_class, *args, **kwargs):
"""A special variant of run_dialog which deletes objects
when a store is reused, it's safe to use when it's disabled,
so always use this in your run_editor hook
"""
retval = run_dialog(dialog_class, parent=self.parent, *args, **kwargs)
if not retval:
# We must return None because of ListDialog's add-item signal
# expects that
retval = None
return retval
#
# Hooks
#
[docs] def run_editor(self, store, model):
"""This can be override by a subclass who wants to send in
custom arguments to the editor.
"""
if self.editor_class is None:
raise ValueError("editor_class cannot be None in %s" % (
type(self).__name__))
return self.run_dialog(
self.editor_class,
store=store, model=model)
[docs] def delete_model(self, model, store):
"""
Deletes the model in a store.
This can be overriden by a subclass which is useful when
you have foreign keys which depends on this class.
:param model: model to delete
:param store: the store to delete the model within
"""
store.remove(model)
[docs]class ModelListDialog(BasicDialog):
"""A dialog which displays all items in a table and allows you to
add and remove items from it
:cvar columns: a list of :class:`kiwi.ui.objectlist.Columns`
:cvar size: a two sized tuple; (width, height) or None
:cvar title: window title
"""
list_slave_class = None
columns = None
size = None
title = None
def __init__(self, store=None, reuse_store=False):
if self.list_slave_class is None:
fmt = "%s needs to set it's list_slave_class attribute"
raise TypeError(fmt % (self.__class__.__name__, ))
self.list_slave = self.list_slave_class(self, store=store,
reuse_store=reuse_store)
BasicDialog.__init__(self, title=self.title, size=self.size)
self.vbox = gtk.VBox()
self.vbox.pack_start(self.list_slave.listcontainer)
self.add(self.vbox)
self.vbox.show()
[docs]class AdditionListSlave(SearchSlave):
"""A slave that offers a simple list and its management.
This slave also has the option to display a small message right next to the
buttons
"""
domain = 'stoq'
toplevel_name = gladefile = 'AdditionListSlave'
widgets = ('add_button',
'delete_button',
'klist',
'list_vbox',
'edit_button')
gsignal('before-edit-item', object, retval=object)
gsignal('on-edit-item', object)
gsignal('on-add-item', object)
gsignal('before-delete-items', object)
gsignal('after-delete-items')
def __init__(self, store, columns=None, editor_class=None,
klist_objects=None, visual_mode=False, restore_name=None,
tree=False):
""" Creates a new AdditionListSlave object
:param store: a store
:param columns: column definitions
:type columns: sequence of :class:`kiwi.ui.objectlist.Columns`
:param editor_class: the window that is going to be open when user
clicks on add_button or edit_button.
:type: editor_class: a :class:`stoqlib.gui.editors.BaseEditor` subclass
:param klist_objects: initial objects to insert into the list
:param visual_mode: if we are working on visual mode, that means,
not possible to edit the model on this object
type visual_mode: bool
:param restore_name: the name used to save and restore the columns
on a cache system (e.g. pickle)
:type restore_name: basestring
:param tree: Indication of which kind of list we are adding.
If `True` ObjectTree otherwise ObjectList will be
added
"""
columns = columns or self.get_columns()
SearchSlave.__init__(self, columns=columns,
restore_name=restore_name,
store=store)
self.tree = tree
self.klist = ObjectTree() if tree else ObjectList()
self.list_vbox.add(self.klist)
self.list_vbox.show_all()
if not self.columns:
raise StoqlibError("columns must be specified")
self.visual_mode = visual_mode
self.store = store
self.set_editor(editor_class)
self._can_edit = True
self._callback_id = None
if self.visual_mode:
self.hide_add_button()
self.hide_edit_button()
self.hide_del_button()
items = klist_objects or self.get_items()
self._setup_klist(items)
self._update_sensitivity()
def _setup_klist(self, klist_objects):
self.klist.set_columns(self.columns)
self.klist.set_selection_mode(gtk.SELECTION_MULTIPLE)
if self.tree:
(self.klist.append(obj.parent_item, obj) for obj in klist_objects)
else:
self.klist.add_list(klist_objects)
if self.visual_mode:
self.klist.set_sensitive(False)
def _update_sensitivity(self, *args):
if self.visual_mode:
return
can_delete = _can_edit = True
objs = self.get_selection()
if not objs:
_can_edit = can_delete = False
elif len(objs) > 1:
_can_edit = False
self.add_button.set_sensitive(True)
self.edit_button.set_sensitive(_can_edit)
self.delete_button.set_sensitive(can_delete)
def _edit_model(self, model=None, parent=None):
edit_mode = model
result = self.emit('before-edit-item', model)
if result is None:
result = self.run_editor(model)
if not result:
return
if edit_mode:
self.emit('on-edit-item', result)
self.klist.update(result)
else:
if self.tree:
self.klist.append(parent, result)
else:
self.klist.append(result)
# Emit the signal after we added the item to the list to be able to
# check the length of the list in our validation callbacks.
self.emit('on-add-item', result)
# As we have a selection extended mode for kiwi list, we
# need to unselect everything before select the new instance.
self.klist.unselect_all()
self.klist.select(result)
self._update_sensitivity()
def _edit(self):
if not self._can_edit:
return
objs = self.get_selection()
qty = len(objs)
if qty != 1:
raise SelectionError(
("Please select only one item before choosing Edit."
"\nThere are currently %d items selected") % qty)
self._edit_model(objs[0])
def _clear(self):
objs = self.get_selection()
qty = len(objs)
if qty < 1:
raise SelectionError('There are no objects selected')
msg = stoqlib_ngettext(
_('Delete this item?'),
_('Delete these %d items?') % qty,
qty)
delete_label = stoqlib_ngettext(
_("Delete item"),
_("Delete items"),
qty)
keep_label = stoqlib_ngettext(
_("Keep it"),
_("Keep them"),
qty)
if not yesno(msg, gtk.RESPONSE_NO, delete_label, keep_label):
return
self.emit('before-delete-items', objs)
if qty == len(self.klist):
self.klist.clear()
else:
for obj in objs:
self.klist.remove(obj)
self.klist.unselect_all()
self._update_sensitivity()
self.emit('after-delete-items')
#
# Hooks
#
[docs] def get_items(self):
return []
[docs] def get_columns(self):
raise NotImplementedError("get_columns must be implemented in "
"subclasses")
[docs] def run_editor(self, model):
"""This can be overriden to provide a custom run_dialog line,
or a conversion function for the model
"""
if self._editor_class is None:
raise TypeError(
"%s cannot create or edit items without the editor_class "
"argument set" % (self.__class__.__name__))
self.store.savepoint('before_run_editor_addition')
retval = run_dialog(self._editor_class, None, store=self.store,
model=model)
if not retval:
self.store.rollback_to_savepoint('before_run_editor_addition')
return retval
[docs] def delete_model(self, model):
"""Deletes a model, can be overridden in subclass
:param model: model to delete
"""
model.__class__.delete(model.id, store=self.store)
#
# Public API
#
[docs] def set_message(self, message, details_callback=None):
"""Display a simple message on a label, next to the add, edit, delete buttons
:param message: a message with properly escaped markup
"""
self.message_hbox.set_visible(True)
self.message_details_button.set_visible(bool(details_callback))
if details_callback:
if self._callback_id:
self.message_details_button.disconnect(self._callback_id)
self._callback_id = self.message_details_button.connect(
'clicked', details_callback)
self.message_label.set_markup(message)
[docs] def clear_message(self):
self.message_hbox.set_visible(False)
[docs] def get_selection(self):
# XXX: add get_selected_rows and raise exceptions if not in the
# right mode
if self.klist.get_selection_mode() == gtk.SELECTION_MULTIPLE:
return self.klist.get_selected_rows()
selection = self.klist.get_selected()
if not selection:
return []
return [selection]
[docs] def set_editor(self, editor_class):
if editor_class and not issubclass(editor_class,
(BaseEditor, BaseWizard)):
raise TypeError("editor_class must be a BaseEditor subclass")
self._editor_class = editor_class
#
# Signal handlers
#
[docs] def on_klist__row_activated(self, *args):
self._edit()
[docs] def on_klist__selection_changed(self, *args):
self._update_sensitivity()
[docs]class SimpleListDialog(BasicDialog):
size = (500, 400)
def __init__(self, columns, objects, hide_cancel_btn=True,
title='', multiple=True, header_text=""):
"""
Create a new SimpleListDialog.
:param columns:
:param objects:
:param hide_cancel_btn:
:param title:
:param multiple: if we're allowed to select multiple items
:type multiple: boolean
"""
BasicDialog.__init__(self, size=self.size, title=title,
header_text=header_text)
if hide_cancel_btn:
self.cancel_button.hide()
if multiple:
selection_mode = gtk.SELECTION_MULTIPLE
else:
selection_mode = gtk.SELECTION_BROWSE
self.setup_slave(columns, objects, selection_mode)
[docs] def setup_slave(self, columns, objects, selection_mode):
self.main.remove(self.main_label)
self._klist = ObjectList(columns, objects, selection_mode)
self.main.add(self._klist)
self._klist.show()
[docs] def get_selection(self):
mode = self._klist.get_selection_mode()
if mode == gtk.SELECTION_MULTIPLE:
return self._klist.get_selected_rows()
selection = self._klist.get_selected()
if not selection:
return []
return [selection]
#
# BasicDialog
#
[docs] def confirm(self):
super(SimpleListDialog, self).confirm()
self.retval = self.retval and self.get_selection()