def get_query_executer(self): """ Fetchs the QueryExecuter for the SearchContainer :returns: a querty executer :rtype: a :class:`QueryExecuter` subclass """ if self._query_executer is None: executer = QueryExecuter(self.store) if not self._lazy_search: executer.set_limit(sysparam.get_int('MAX_SEARCH_RESULTS')) if self._search_spec is not None: executer.set_search_spec(self._search_spec) self._query_executer = executer return self._query_executer
def get_query_executer(self): """ Fetchs the QueryExecuter for the SearchContainer :returns: a querty executer :rtype: a :class:`QueryExecuter` subclass """ if self._query_executer is None: executer = QueryExecuter(self.store) if not self._lazy_search: executer.set_limit(sysparam.get_int('MAX_SEARCH_RESULTS')) if self._search_spec is not None: executer.set_search_spec(self._search_spec) self._query_executer = executer return self._query_executer
class ShellApp(GladeDelegate): """Base class for shell applications. The main use is to interact with a shell window and reduce duplication between other applications. """ #: This attribute is used when generating titles for applications. #: It's also useful if we get a list of available applications with #: the application names translated. This list is going to be used when #: creating new user profiles. app_title = None #: name of the application, 'pos', 'payable', etc app_name = None #: If this application has a search like interface search = None #: This dictionary holds information about required permissions to access #: certain actions. Keys should be the name of the action (for instance #: SearchEmployess), and the value should be a tuple with the permission key #: (domain object or action identifier) and the required permission. In this #: case: ('Employee', perm.PERM_SEARCH). See <stoqlib.lib.permissions> action_permissions = {} #: The table we will query on to perform the search search_table = None #: Label left of the search entry search_label = _('Search:') #: the report class for printing the object list embedded on app. report_table = None def __init__(self, window, store=None): if store is None: store = api.get_default_store() self.store = store self.window = window self._loading_filters = False self._sensitive_group = dict() self.help_ui = None self.uimanager = self.window.uimanager self._pre_init() GladeDelegate.__init__(self, gladefile=self.gladefile, toplevel_name=self.toplevel_name) self._post_init() def _pre_init(self): # FIXME: Perhaps we should add a proper API to add a search to # an application, however it's a bit complicated since the # search creation is done in two steps due to how kiwi auto # signal connection works if self.search_table is not None: self._create_search() self._app_settings = api.user_settings.get('app-ui', {}) # Create actions, this must be done before the constructor # is called, eg when signals are autoconnected self.create_actions() def _post_init(self): self.create_ui() if self.search_table is not None: self.attach_slave('search_holder', self.search) self.create_filters() self._restore_filter_settings() self.search.focus_search_entry() def _create_search(self): # This does the first part of the search creation, # this need to be done here so that self.results is set when we # call GladeDelegate.__init__() self.executer = QueryExecuter(self.store) # FIXME: Remove this limit, but we need to migrate all existing # searches to use lazy lists first. That in turn require # us to rewrite the queries in such a way that count(*) # will work properly. self.executer.set_limit(sysparam(self.store).MAX_SEARCH_RESULTS) self.executer.set_table(self.search_table) self.search = SearchSlave(self.get_columns(), restore_name=self.__class__.__name__) self.search.enable_advanced_search() self.search.set_query_executer(self.executer) self.search.connect("search-completed", self._on_search__search_completed) self.results = self.search.result_view search_filter = self.search.get_primary_filter() search_filter.set_label(self.search_label) def _display_open_inventory_message(self): msg = _(u'There is an inventory process open at the moment.\n' 'While that inventory is open, you will be unable to do ' 'operations that modify your stock.') self.inventory_bar = self.window.add_info_bar(gtk.MESSAGE_WARNING, msg) def _save_filter_settings(self): if self._loading_filters: return filter_states = self.search.get_filter_states() settings = self._app_settings.setdefault(self.app_name, {}) settings['filter-states'] = filter_states def _restore_filter_settings(self): self._loading_filters = True settings = self._app_settings.setdefault(self.app_name, {}) filter_states = settings.get('filter-states') if filter_states is not None: # Disable auto search to avoid an extra query when restoring the # state self.search.set_auto_search(False) self.search.set_filter_states(filter_states) self.search.set_auto_search(True) self._loading_filters = False # # Overridables # def create_actions(self): """This is called before the BaseWindow constructor, so we can create actions that can be autoconnected. The widgets and actions loaded from builder files are not set yet""" def create_ui(self): """This is called when the UI such as GtkWidgets should be created. Glade widgets are now created and can be accessed in the instance. """ def activate(self, params): """This is when you switch to an application. You should setup widget sensitivity here and refresh lists etc :params params: an dictionary with optional parameters. """ def setup_focus(self): """Define this method on child when it's needed. This is for calling grab_focus(), it's called after the window is shown. focus chains should be created in create_ui()""" def get_title(self): # This method must be redefined in child when it's needed branch = api.get_current_branch(self.store) return _('[%s] - %s') % (branch.get_description(), self.app_title) def can_change_application(self): """Define if we can change the current application or not. :returns: True if we can change the application, False otherwise. """ return True def can_close_application(self): """Define if we can close the current application or not. :returns: True if we can close the application, False otherwise. """ return True def set_open_inventory(self): """ Subclasses should overide this if they call :obj:`.check_open_inventory`. This method will be called it there is an open inventory, so the application can disable some funcionalities """ raise NotImplementedError def new_activate(self): """Called when the New toolbar item is activated""" raise NotImplementedError def search_activate(self): """Called when the Search toolbar item is activated""" raise NotImplementedError def print_activate(self): """Called when the Print toolbar item is activated""" if self.search_table is None: raise NotImplementedError if self.results.get_selection_mode() == gtk.SELECTION_MULTIPLE: results = self.results.get_selected_rows() else: result = self.results.get_selected() results = [result] if result else None # There are no itens selected. We should print the entire list if not results: results = list(self.search.get_last_results()) self.print_report(self.report_table, self.results, results) def export_spreadsheet_activate(self): """Called when the Export menu item is activated""" if self.search_table is None: raise NotImplementedError sse = SpreadSheetExporter() sse.export(object_list=self.results, name=self.app_name, filename_prefix=self.app.name) def create_filters(self): """Implement this to provide filters for the search container""" def search_completed(self, results, states): """Implement this if you want to know when a search has been completed. :param results: the search results :param states: search states used to construct the search query search """ # # Public API # def add_ui_actions(self, ui_string, actions, name='Actions', action_type='normal', filename=None): return self.window.add_ui_actions(ui_string=ui_string, actions=actions, name=name, action_type=action_type, filename=filename, instance=self) def add_tool_menu_actions(self, actions): return self.window.add_tool_menu_actions(actions=actions) def set_help_section(self, label, section): self.window.set_help_section(label=label, section=section) def get_statusbar_message_area(self): return self.window.statusbar.message_area def print_report(self, report_class, *args, **kwargs): filters = self.search.get_search_filters() if filters: kwargs['filters'] = filters print_report(report_class, *args, **kwargs) def set_sensitive(self, widgets, value): """Set the *widgets* sensitivity based on *value* If a sensitive group was registered for any widget, it's validation function will be tested and, if ``False`` is returned, it will be set insensitive, ignoring *value* :param widgets: a list of widgets :param value: either `True` or `False` """ # FIXME: Maybe this should ne done on kiwi? for widget in widgets: sensitive = value for validator in self._sensitive_group.get(widget, []): if not validator[0](*validator[1]): sensitive = False break widget.set_sensitive(sensitive) def register_sensitive_group(self, widgets, validation_func, *args): """Register widgets on a sensitive group. Everytime :obj:`.set_sensitive()` is called, if there is any validation function for a given widget on sensitive group, then that will be used to decide if it gets sensitive or insensitive. :param widgets: a list of widgets :param validation_func: a function for validation. It should return either ``True`` or ``False``. :param args: args that will be passed to *validation_func* """ assert callable(validation_func) for widget in widgets: validators = self._sensitive_group.setdefault(widget, set()) validators.add((validation_func, args)) def run_dialog(self, dialog_class, *args, **kwargs): """ Encapsuled method for running dialogs. """ return run_dialog(dialog_class, self, *args, **kwargs) @cached_function() def has_open_inventory(self): return Inventory.has_open(self.store, api.get_current_branch(self.store)) def check_open_inventory(self): """Checks if there is an open inventory. In the case there is one, will call set_open_inventory (subclasses should implement it). Returns True if there is an open inventory. False otherwise """ inventory_bar = getattr(self, 'inventory_bar', None) if self.has_open_inventory(): if inventory_bar: inventory_bar.show() else: self._display_open_inventory_message() self.set_open_inventory() return True elif inventory_bar: inventory_bar.hide() return False # FIXME: Most of these should be removed and access the search API # directly, eg, self.search.clear() etc def add_filter(self, search_filter, position=SearchFilterPosition.BOTTOM, columns=None, callback=None): """ See :class:`SearchSlave.add_filter` """ self.search.add_filter(search_filter, position, columns, callback) def set_text_field_columns(self, columns): """ See :class:`SearchSlave.set_text_field_columns` """ self.search.set_text_field_columns(columns) def refresh(self): """ See :class:`stoqlib.gui.search.searchslave.SearchSlave.refresh` """ self.search.refresh() def clear(self): """ See :class:`stoqlib.gui.search.searchslave.SearchSlave.clear` """ self.search.clear() def select_result(self, result): """Select the object in the result list If the object is not in the list (filtered out, for instance), no error is thrown and nothing is selected """ try: self.results.select(result) except ValueError: pass # # Callbacks # def _on_search__search_completed(self, search, results, states): self.search_completed(results, states) has_results = len(results) for widget in [self.window.Print, self.window.ExportSpreadSheet]: widget.set_sensitive(has_results) self._save_filter_settings()
class SearchDialog(BasicDialog): """ Base class for *all* the search dialogs, responsible for the list construction and "Filter" and "Clear" buttons management. This class must be subclassed and its subclass *must* implement the methods 'get_columns' and 'get_query_and_args' (if desired, 'get_query_and_args' can be implemented in the user's slave class, so SearchDialog will get its slave instance and call the method directly). Its subclass also must implement a setup_slaves method and call its equivalent base class method as in: >>> def setup_slave(self): ... SearchDialog.setup_slaves(self) or then, call it in its constructor, like: >>> def __init__(self, *args): ... SearchDialog.__init__(self) """ main_label_text = '' #: Title that will appear in the window, for instance 'Product Search' title = '' # The table type which we will query on to get the objects. search_table = None #: The label that will be used for the main filter in this dialog search_label = None #: Selection mode to use (if its possible to select more than one row) selection_mode = gtk.SELECTION_BROWSE #: Default size for this dialog size = () #: If the advanced search is enabled or disabled. When ``True`` we will #: instrospect the columns returned by :meth:`get_columns`,and use those #: that are subclasses of :class:`stoqlib.gui.columns.SearchColumn` to add #: as options for the user to filter the results. advanced_search = True tree = False def __init__(self, store, search_table=None, hide_footer=True, title='', selection_mode=None, double_click_confirm=False): """ A base class for search dialog inheritance :param store: a store :param table: :param search_table: :param hide_footer: :param title: :param selection_mode: :param double_click_confirm: If double click a item in the list should automatically confirm """ self.store = store self.search_table = search_table or self.search_table if not self.search_table: raise ValueError("%r needs a search table" % self) self.selection_mode = self._setup_selection_mode(selection_mode) self.summary_label = None self.double_click_confirm = double_click_confirm BasicDialog.__init__(self, hide_footer=hide_footer, main_label_text=self.main_label_text, title=title or self.title, size=self.size) self.executer = QueryExecuter(store) # FIXME: Remove this limit, but we need to migrate all existing # searches to use lazy lists first. That in turn require # us to rewrite the queries in such a way that count(*) # will work properly. self.executer.set_limit(sysparam(self.store).MAX_SEARCH_RESULTS) self.set_table(self.search_table) self.enable_window_controls() self.disable_ok() self.set_ok_label(_('Se_lect Items')) self._setup_search() self._setup_details_slave() self.create_filters() self.setup_widgets() if self.search_label: self.set_searchbar_label(self.search_label) def _setup_selection_mode(self, selection_mode): # For consistency do not allow none or single, in other words, # only allowed values are browse and multiple so we always will # be able to use both the keyboard and the mouse to select items # in the search list. selection_mode = selection_mode or self.selection_mode if (selection_mode != gtk.SELECTION_BROWSE and selection_mode != gtk.SELECTION_MULTIPLE): raise ValueError('Invalid selection mode %r' % selection_mode) return selection_mode def _setup_search(self): self.search = SearchSlave( self.get_columns(), tree=self.tree, restore_name=self.__class__.__name__, ) self.search.set_query_executer(self.executer) if self.advanced_search: self.search.enable_advanced_search() self.attach_slave('main', self.search) self.header.hide() self.results = self.search.result_view self.results.set_selection_mode(self.selection_mode) self.results.connect('cell-edited', self._on_results__cell_edited) self.results.connect('selection-changed', self._on_results__selection_changed) self.results.connect('row-activated', self._on_results__row_activated) def _setup_details_slave(self): # FIXME: Gross hack has_details_btn = hasattr(self, 'on_details_button_clicked') has_print_btn = hasattr(self, 'on_print_button_clicked') if not (has_details_btn or has_print_btn): self._details_slave = None return self._details_slave = _SearchDialogDetailsSlave() self.attach_slave('details_holder', self._details_slave) if has_details_btn: self._details_slave.connect("details", self.on_details_button_clicked) else: self._details_slave.details_button.hide() if has_print_btn: self._details_slave.connect("print", self.on_print_button_clicked) self.set_print_button_sensitive(False) self.results.connect('has-rows', self._has_rows) else: self._details_slave.print_button.hide() # # Public API # def add_button(self, label, stock=None, image=None): """Adds a button in the bottom of the dialog. :param label: the text that will be displayed by the button. :param stock: the gtk stock id to be used in the button. :param image: the image filename. """ button = gtk.Button(label=label) if image: image_widget = gtk.Image() image_widget.set_from_file( environ.find_resource('pixmaps', image)) image_widget.show() button.set_image(image_widget) elif stock: button_set_image_with_label(button, stock, label) self.action_area.set_layout(gtk.BUTTONBOX_START) self.action_area.pack_start(button, False, False, 6) return button def set_details_button_sensitive(self, value): self._details_slave.details_button.set_sensitive(value) def set_print_button_sensitive(self, value): self._details_slave.print_button.set_sensitive(value) def get_selection(self): mode = self.results.get_selection_mode() if mode == gtk.SELECTION_BROWSE: return self.results.get_selected() return self.results.get_selected_rows() def confirm(self, retval=None): """Confirms the dialog :param retval: optional parameter which will be selected when the dialog is closed """ if retval is None: retval = self.get_selection() self.retval = retval self.search.save_columns() # FIXME: This should chain up so the "confirm" signal gets emitted self.close() def cancel(self, *args): self.retval = [] self.search.save_columns() # FIXME: This should chain up so the "cancel" signal gets emitted self.close() def set_table(self, table): self.executer.set_table(table) self.search_table = table # FIXME: -> remove/use # TODO: Check if we can remove def set_searchbar_label(self, label): search_filter = self.search.get_primary_filter() search_filter.set_label(label) def set_searchbar_search_string(self, string): if string == self.get_searchbar_search_string(): return search_filter = self.search.get_primary_filter() search_filter.entry.set_text(string) def get_searchbar_search_string(self): search_filter = self.search.get_primary_filter() return search_filter.get_state().text def set_text_field_columns(self, columns): """See :class:`SearchSlave.set_text_field_columns` """ self.search.set_text_field_columns(columns) def disable_search_entry(self): """See :class:`SearchSlave.disable_search_entry` """ self.search.disable_search_entry() def add_filter(self, search_filter, position=SearchFilterPosition.BOTTOM, columns=None, callback=None): """See :class:`SearchSlave.add_filter` """ self.search.add_filter(search_filter, position, columns, callback) def row_activate(self, obj): """This is called when an item in the results list is double clicked. :param obj: the item that was double clicked. """ if self.double_click_confirm: # But only if its also confirmable with ok_button if self.ok_button.props.sensitive: self.confirm() # # Filters # def create_branch_filter(self, label=None): from stoqlib.domain.person import Branch branches = Branch.get_active_branches(self.store) items = [(b.person.name, b.id) for b in branches] # if not items: # raise ValueError('You should have at least one branch at ' # 'this point') items.insert(0, (_("Any"), None)) if not label: label = _('Branch:') branch_filter = ComboSearchFilter(label, items) current = api.get_current_branch(self.store) if current: branch_filter.select(current.id) return branch_filter def create_payment_filter(self, label=None): from stoqlib.domain.payment.method import PaymentMethod methods = PaymentMethod.get_active_methods(self.store) items = [(_('Any'), None)] for method in methods: if method.method_name == 'multiple': continue items.append((method.description, method)) if not label: label = _('Method:') payment_filter = ComboSearchFilter(label, items) payment_filter.select(None) return payment_filter def create_provider_filter(self, label=None): from stoqlib.domain.payment.card import CreditProvider providers = CreditProvider.get_card_providers(self.store) items = [(p.short_name, p) for p in providers] items.insert(0, (_("Any"), None)) if not label: label = _('Provider:') provider_filter = ComboSearchFilter(label, items) return provider_filter # # Callbacks # def on_search__search_completed(self, search, results, states): self.search_completed(results, states) def _on_results__cell_edited(self, results, obj, attr): """Override this method on child when it's needed to perform some tasks when editing a row. """ def _on_results__selection_changed(self, results, selected): self.update_widgets() if selected: self.enable_ok() else: self.disable_ok() def _on_results__row_activated(self, results, obj): self.row_activate(obj) def _has_rows(self, results, obj): self.set_print_button_sensitive(obj) # # Hooks # def create_filters(self): raise NotImplementedError( "create_filters() must be implemented in %r" % self) def setup_widgets(self): pass def get_columns(self): raise NotImplementedError( "get_columns() must be implemented in %r" % self) def update_widgets(self): """Subclass can have an 'update_widgets', and this method will be called when a signal is emitted by 'Filter' or 'Clear' buttons and also when a list item is selected. """ def search_completed(self, results, states): pass