def enable_lazy_search(self): self._lazy_updater = LazyObjectListUpdater( executer=self._query_executer, search=self) # Limits doesn't make sense when using lazy search, the idea # is to always show everything. self._query_executer.set_limit(-1)
class SearchContainer(gtk.VBox): """ A search container is a widget which consists of: - search entry (w/ a label) (:class:`StringSearchFilter`) - search button - objectlist result (:class:`SearchResults` or class:`SearchResultsTree) - a query executer (:class:`kiwi.db.query.QueryExecuter`) Additionally you can add a number of search filters to the SearchContainer. You can chose if you want to add the filter in the top-left corner of bottom, see :class:`SearchFilterPosition` """ __gtype_name__ = 'SearchContainer' filter_label = gobject.property(type=str) results_class = SearchResults gsignal("search-completed", object, object) def __init__(self, columns=None, tree=False, chars=25): """ Create a new SearchContainer object. :param columns: a list of :class:`kiwi.ui.objectlist.Column` :param tree: if we should list the results as a tree :param chars: maximum number of chars used by the search entry """ if tree: self.results_class = SearchResultsTree gtk.VBox.__init__(self) self._auto_search = True self._columns = columns self._lazy_updater = None self._model = None self._query_executer = None self._search_filters = [] self._summary_label = None self.menu = None search_filter = StringSearchFilter(_('Search:'), chars=chars, container=self) search_filter.connect('changed', self._on_search_filter__changed) self._search_filters.append(search_filter) self._primary_filter = search_filter self._create_ui() # # GObject # def do_set_property(self, pspec, value): if pspec.name == 'filter-label': self._primary_filter.set_label(value) else: raise AssertionError(pspec.name) def do_get_property(self, pspec): if pspec.name == 'filter-label': return self._primary_filter.get_label() else: raise AssertionError(pspec.name) # # Public API # def add_filter(self, search_filter, position=SearchFilterPosition.BOTTOM, columns=None, callback=None): """ Adds a search filter :param search_filter: the search filter :param postition: a :class:`SearchFilterPosition` enum :param columns: :param callback: """ if not isinstance(search_filter, SearchFilter): raise TypeError("search_filter must be a SearchFilter subclass, " "not %r" % (search_filter,)) if columns and callback: raise TypeError("Cannot specify both column and callback") executer = self.get_query_executer() if executer: if columns: executer.set_filter_columns(search_filter, columns) if callback: if not callable(callback): raise TypeError("callback must be callable") executer.add_filter_query_callback(search_filter, callback) else: if columns or callback: raise TypeError( "You need to set an executor before calling set_filters " "with columns or callback set") assert not search_filter.get_parent() self.set_filter_position(search_filter, position) search_filter.connect('changed', self._on_search_filter__changed) search_filter.connect('removed', self._on_search_filter__remove) self._search_filters.append(search_filter) def remove_filter(self, filter): self.filters_box.remove(filter) self._search_filters.remove(filter) filter.destroy() if self._auto_search: self.search() def add_filter_by_column(self, column): """Add a filter accordingly to the column specification :param column: a SearchColumn instance """ title = (column.long_title or column.title) + ':' if column.data_type == datetime.date: filter = DateSearchFilter(title) if column.valid_values: filter.clear_options() filter.add_custom_options() for opt in column.valid_values: filter.add_option(opt) filter.select(column.valid_values[0]) elif (column.data_type == Decimal or column.data_type == int or column.data_type == currency): filter = NumberSearchFilter(title) if column.data_type != int: filter.set_digits(2) elif column.data_type == str: if column.valid_values: filter = ComboSearchFilter(title, column.valid_values) else: filter = StringSearchFilter(title) filter.enable_advanced() else: # TODO: Boolean raise NotImplementedError(title, column.data_type) filter.set_removable() attr = column.search_attribute or column.attribute self.add_filter(filter, columns=[attr]) label = filter.get_title_label() label.set_alignment(1.0, 0.5) self.label_group.add_widget(label) combo = filter.get_mode_combo() if combo: self.combo_group.add_widget(combo) return filter def get_search_filters(self): return self._search_filters def get_search_filter_by_label(self, label): for search_filter in self._search_filters: if search_filter.label == label: return search_filter def set_filter_position(self, search_filter, position): """ Set the the filter position. :param search_filter: :param position: """ if search_filter.get_parent(): search_filter.get_parent().remove(search_filter) if position == SearchFilterPosition.TOP: self.hbox.pack_start(search_filter, False, False) self.hbox.reorder_child(search_filter, 0) elif position == SearchFilterPosition.BOTTOM: self.filters_box.pack_start(search_filter, False, False) search_filter.show() def get_filter_position(self, search_filter): """ Get filter by position. :param search_filter: """ if search_filter.get_parent() == self.hbox: return SearchFilterPosition.TOP elif search_filter.get_parent() == self: return SearchFilterPosition.BOTTOM else: raise AssertionError(search_filter) def set_query_executer(self, query_executer): """ Ties a QueryExecuter instance to the SearchContainer class :param querty_executer: a querty executer :type querty_executer: a :class:`QueryExecuter` subclass """ if not isinstance(query_executer, QueryExecuter): raise TypeError("query_executer must be a QueryExecuter instance") self._query_executer = query_executer def get_query_executer(self): """ Fetchs the QueryExecuter for the SearchContainer :returns: a querty executer :rtype: a :class:`QueryExecuter` subclass """ return self._query_executer def get_primary_filter(self): """ Fetches the primary filter for the SearchContainer. The primary filter is the filter attached to the standard entry normally used to do free text searching :returns: the primary filter """ return self._primary_filter def search(self, clear=True): """ Starts a search. Fetches the states of all filters and send it to a query executer and finally puts the result in the result class """ if not self._query_executer: raise ValueError("A query executer needs to be set at this point") states = [(sf.get_state()) for sf in self._search_filters] results = self._query_executer.search(states) self.add_results(results, clear=clear) self.emit("search-completed", self.results, states) if self._summary_label: if self._lazy_updater and len(self.results): post = self.results.get_model().get_post_data() if post is not None: self._summary_label.update_total(post.sum) else: self._summary_label.update_total() def enable_lazy_search(self): self._lazy_updater = LazyObjectListUpdater( executer=self._query_executer, search=self) # Limits doesn't make sense when using lazy search, the idea # is to always show everything. self._query_executer.set_limit(-1) def set_auto_search(self, auto_search): """ Enables/Disables auto search which means that the search result box is automatically populated when a filter changes :param auto_search: True to enable, False to disable """ self._auto_search = auto_search def set_text_field_columns(self, columns): if self._primary_filter is None: raise ValueError("The primary filter is disabled") if not self._query_executer: raise ValueError("A query executer needs to be set at this point") self._query_executer.set_filter_columns(self._primary_filter, columns) def disable_search_entry(self): """ Disables the search entry """ self.search_entry.hide() self._primary_filter.hide() self._search_filters.remove(self._primary_filter) self._primary_filter = None def set_summary_label(self, column, label='Total:', format='%s', parent=None): """ Adds a summary label to the result set :param column: the column to sum from :param label: the label to use, defaults to 'Total:' :param format: the format, defaults to '%%s', must include '%%s' :param parent: the parent widget a label should be added to or None if it should be added to the SearchContainer """ if not '%s' in format: raise ValueError("format must contain %s") try: self.results.get_column_by_name(column) except LookupError: raise ValueError("%s is not a valid column" % (column,)) if not parent: parent = self elif not isinstance(parent, gtk.Box): raise TypeError("parent %r must be a GtkBox subclass" % ( parent)) if self._summary_label: self._summary_label.get_parent().remove(self._summary_label) if self._lazy_updater: summary_label_class = LazySummaryLabel else: summary_label_class = SummaryLabel self._summary_label = summary_label_class(klist=self.results, column=column, label=label, value_format=format) parent.pack_start(self._summary_label, False, False) self._summary_label.show() @property def summary_label(self): return self._summary_label def enable_advanced_search(self): self._create_advanced_search() def add_results(self, results, clear=True): if clear: self.results.clear() if self._lazy_updater: self._lazy_updater.add_results(results) else: self.results.add_results(results) def get_filter_states(self): dict_state = {} for search_filter in self._search_filters: dict_state[search_filter.label] = data = {} state = search_filter.get_state() if isinstance(state, DateQueryState): data['start'] = state.date elif isinstance(state, DateIntervalQueryState): data['start'] = state.start data['end'] = state.end elif isinstance(state, NumberQueryState): data['value'] = state.value if hasattr(state, 'value_id'): data['value_id'] = state.value_id data['value'] = None elif isinstance(state, NumberIntervalQueryState): data['start'] = state.start data['end'] = state.end elif isinstance(state, StringQueryState): data['text'] = state.text data['mode'] = state.mode else: raise NotImplementedError(state) return dict_state def set_filter_states(self, dict_state): for label, filter_state in dict_state.items(): search_filter = self.get_search_filter_by_label(label) if search_filter is None: continue search_filter.set_state(**filter_state) # # Callbacks # def _on_search_button__clicked(self, button): self.search() def _on_search_entry__activate(self, button): self.search() def _on_search_filter__remove(self, filter): self.remove_filter(filter) def _on_search_filter__changed(self, search_filter): if self._auto_search: self.search() # # Private # def _create_ui(self): self._create_basic_search() self.results = self.results_class(self._columns) self.pack_start(self.results, True, True, 0) self.results.show() def _create_basic_search(self): filters_box = gtk.VBox() filters_box.show() self.pack_start(filters_box, expand=False) hbox = gtk.HBox() hbox.set_border_width(3) filters_box.pack_start(hbox, False, False) hbox.show() self.hbox = hbox widget = self._primary_filter self.hbox.pack_start(widget, False, False) widget.show() self.search_entry = self._primary_filter.entry self.search_entry.connect('activate', self._on_search_entry__activate) self.search_button = SearchFilterButton(stock=gtk.STOCK_FIND) self.search_button.connect('clicked', self._on_search_button__clicked) hbox.pack_start(self.search_button, False, False) self.search_button.show() self.filters_box = filters_box def _create_advanced_search(self): self.label_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) self.combo_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) self.menu = gtk.Menu() for column in self._columns: if not isinstance(column, SearchColumn): continue if column.data_type not in (datetime.date, Decimal, int, currency, str): continue title = column.long_title or column.title menu_item = gtk.MenuItem(title) menu_item.set_data('column', column) menu_item.show() menu_item.connect('activate', self._on_menu_item_activate) self.menu.append(menu_item) def _on_menu_item_activate(self, item): column = item.get_data('column') if column is None: return self.add_filter_by_column(column)