def _setup_widgets(self): self._replace_widget() # Add the two buttons self.find_button = self._create_button(gtk.STOCK_FIND) self.edit_button = self._create_button(gtk.STOCK_NEW) can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.find_button.set_tooltip_text(self.find_tooltip) self.edit_button.set_tooltip_text(self.new_tooltip) # the entry needs a completion to work in MODE_DATA self._completion = gtk.EntryCompletion() self._entry.set_completion(self._completion) self._entry.set_mode(ENTRY_MODE_DATA) initial_value = getattr(self._model, self._model_property) self.set_value(initial_value) # The filter that will be used. This is not really in the interface. We # will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self._search_class.search_spec) self._executer.set_filter_columns(self._filter, self._search_columns)
def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._current_obj = None self._parent = parent self._on_run_editor = run_editor self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.SEARCH_SPEC) self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS) self._last_operation = None self._source_id = None self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor) self._setup() self.set_value(initial_value, force=True)
def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None, edit_button=None, info_button=None, search_clause=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._parent = parent self._on_run_editor = run_editor self._can_edit = False self._search_clause = search_clause self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.edit_button = edit_button self.info_button = info_button self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.search_spec) self._executer.set_filter_columns(self._filter, self.search_columns) self._last_operation = None self._source_id = None self._setup() self.set_value(initial_value, force=True)
def _create_search(self): self.search = SearchSlave(self._get_columns(self.model.kind)) self.search.connect('result-item-activated', self._on_search__item_activated) self.search.enable_advanced_search() self.query = QueryExecuter(self.app.store) self.search.set_query_executer(self.query) self.search.set_result_view(FinancialSearchResults) self.result_view = self.search.result_view self.result_view.page = self tree_view = self.search.result_view.get_treeview() tree_view.set_rules_hint(True) tree_view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)
def setup_slaves(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__) self.search.enable_advanced_search() self.attach_slave('place_holder', self.search) self.executer = QueryExecuter(self.store) self.search.set_query_executer(self.executer) self.executer.set_table(LoanView) self.executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.results.connect('selection-changed', self._on_results_selection_changed) self.search.results.set_selection_mode(gtk.SELECTION_MULTIPLE) self.search.focus_search_entry()
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 _create_search(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__) self.search.enable_advanced_search() self.attach_slave('searchbar_holder', self.search) self.executer = QueryExecuter(self.store) self.search.set_query_executer(self.executer) self.executer.set_table(PurchaseOrderView) self.executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.results.connect('selection-changed', self._on_results__selection_changed) self.search.results.connect('row-activated', self._on_results__row_activated) self.search.focus_search_entry()
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 _date_query(self, search_spec, column): sfilter = object() executer = QueryExecuter(self.store) executer.set_filter_columns(sfilter, [column]) executer.set_search_spec(search_spec) state = DateIntervalQueryState(filter=sfilter, start=self.start, end=self.end) return executer.search([state])
def _setup_slaves(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__) self.attach_slave('search_group_holder', self.search) executer = QueryExecuter(self.store) executer.set_table(QuotationView) self.search.set_query_executer(executer) self.search.set_text_field_columns(['supplier_name']) filter = self.search.get_primary_filter() filter.set_label(_(u'Supplier:')) self.search.focus_search_entry() self.search.results.connect('selection-changed', self._on_searchlist__selection_changed) self.search.results.connect('row-activated', self._on_searchlist__row_activated) date_filter = DateSearchFilter(_('Date:')) self.search.add_filter(date_filter, columns=['open_date', 'deadline']) self.edit_button.set_sensitive(False) self.remove_button.set_sensitive(False)
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)
class QueryExecuterTest(DomainTest): def setUp(self): DomainTest.setUp(self) self.qe = QueryExecuter(self.store) self.qe.set_search_spec(ClientCategory) self.sfilter = mock.Mock() self.qe.set_filter_columns(self.sfilter, ['name']) def _search_string_all(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_ALL, text=text)]) def _search_string_exactly(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_EXACTLY, text=text)]) def _search_string_not(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.NOT_CONTAINS, text=text)]) def test_string_query(self): self.assertEquals(self.store.find(ClientCategory).count(), 0) self.create_client_category(u'EYE MOON FLARE 110 0.5') self.create_client_category(u'EYE MOON FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 110 1.0') self.create_client_category(u'EYE SUN STONE 120 0.5') self.assertEquals(self._search_string_all(u'eye flare 110').count(), 2) self.assertEquals(self._search_string_all(u'eye 0.5').count(), 2) self.assertEquals(self._search_string_all(u'eye 120').count(), 3) self.assertEquals(self._search_string_exactly(u'eye flare 110').count(), 0) self.assertEquals(self._search_string_exactly(u'eye 0.5').count(), 0) self.assertEquals(self._search_string_exactly(u'eye 120').count(), 0) self.assertEquals(self._search_string_not(u'stone 110').count(), 2) self.assertEquals(self._search_string_not(u'eye').count(), 0) self.assertEquals(self._search_string_not(u'moon 120').count(), 1)
def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None, edit_button=None, info_button=None, search_clause=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._parent = parent self._on_run_editor = run_editor self._can_edit = False self._search_clause = search_clause self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.edit_button = edit_button self.info_button = info_button self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.search_spec) self._executer.set_filter_columns(self._filter, self.search_columns) self._executer.set_order_by(self.order_by) self._last_operation = None self._source_id = None self._setup() self.set_value(initial_value, force=True)
class PurchaseSelectionStep(BaseWizardStep): gladefile = 'PurchaseSelectionStep' def __init__(self, wizard, store): self._next_step = None BaseWizardStep.__init__(self, store, wizard) self.setup_slaves() def _refresh_next(self, validation_value): has_selection = self.search.results.get_selected() is not None self.wizard.refresh_next(has_selection) def _create_search(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__) self.search.enable_advanced_search() self.attach_slave('searchbar_holder', self.search) self.executer = QueryExecuter(self.store) self.search.set_query_executer(self.executer) self.executer.set_table(PurchaseOrderView) self.executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.results.connect('selection-changed', self._on_results__selection_changed) self.search.results.connect('row-activated', self._on_results__row_activated) self.search.focus_search_entry() def _create_filters(self): self.search.set_text_field_columns(['supplier_name']) def get_extra_query(self, states): return PurchaseOrderView.status == PurchaseOrder.ORDER_CONFIRMED def _get_columns(self): return [ IdentifierColumn('identifier', sorted=True), SearchColumn('open_date', title=_('Date Started'), data_type=datetime.date, width=100), SearchColumn('expected_receival_date', data_type=datetime.date, title=_('Expected Receival'), visible=False), SearchColumn('supplier_name', title=_('Supplier'), data_type=str, searchable=True, width=130, expand=True), SearchColumn('ordered_quantity', title=_('Qty Ordered'), data_type=Decimal, width=110, format_func=format_quantity), SearchColumn('received_quantity', title=_('Qty Received'), data_type=Decimal, width=145, format_func=format_quantity), SearchColumn('total', title=_('Order Total'), data_type=currency, width=120) ] def _update_view(self): has_selection = self.search.results.get_selected() is not None self.details_button.set_sensitive(has_selection) # # WizardStep hooks # def post_init(self): self._update_view() self.register_validate_function(self._refresh_next) self.force_validation() def next_step(self): self.search.save_columns() selected = self.search.results.get_selected() purchase = selected.purchase # We cannot create the model in the wizard since we haven't # selected a PurchaseOrder yet which ReceivingOrder depends on # Create the order here since this is the first place where we # actually have a purchase selected if not self.wizard.model: self.wizard.model = self.model = ReceivingOrder( responsible=api.get_current_user(self.store), supplier=purchase.supplier, invoice_number=None, branch=purchase.branch, purchase=purchase, store=self.store) # Remove all the items added previously, used if we hit back # at any point in the wizard. if self.model.purchase != purchase: self.model.remove_items() # This forces ReceivingOrderProductStep to create a new model self._next_step = None if selected: self.model.purchase = purchase self.model.branch = purchase.branch self.model.supplier = purchase.supplier self.model.transporter = purchase.transporter else: self.model.purchase = None # FIXME: Improve the infrastructure to avoid this local caching of # Wizard steps. if not self._next_step: # Remove all the items added previously, used if we hit back # at any point in the wizard. self._next_step = ReceivingOrderItemStep(self.store, self.wizard, self.model, self) return self._next_step def has_previous_step(self): return False def setup_slaves(self): self._create_search() # # Kiwi callbacks # # def on_searchbar_activate(self, slave, objs): # """Use this callback with SearchBar search-activate signal""" # self.results.add_list(objs, clear=True) # has_selection = self.results.get_selected() is not None # self.wizard.refresh_next(has_selection) def _on_results__selection_changed(self, results, purchase_order_view): self.force_validation() self._update_view() def _on_results__row_activated(self, results, purchase_order_view): run_dialog(PurchaseDetailsDialog, self.wizard, self.store, model=purchase_order_view.purchase) def on_details_button__clicked(self, *args): selected = self.search.results.get_selected() if not selected: raise ValueError('You should have one order selected ' 'at this point, got nothing') run_dialog(PurchaseDetailsDialog, self.wizard, self.store, model=selected.purchase)
def _date_filter_query(self, search_table, column): executer = QueryExecuter(self.store) executer.set_filter_columns(self.date_filter, [column]) executer.set_table(search_table) return executer.search([self.date_filter.get_state()])
def setUp(self): DomainTest.setUp(self) self.qe = QueryExecuter(self.store) self.qe.set_search_spec(ClientCategory) self.sfilter = mock.Mock() self.qe.set_filter_columns(self.sfilter, ['name'])
class TransactionPage(object): # shows either a list of: # - transactions # - payments def __init__(self, model, app, parent): self.model = model self.app = app self.parent_window = parent self._block = False self._create_search() self._add_date_filter() self._setup_search() self.refresh() def get_toplevel(self): return self.parent_window def _create_search(self): self.search = SearchSlave(self._get_columns(self.model.kind)) self.search.connect('result-item-activated', self._on_search__item_activated) self.search.enable_advanced_search() self.query = QueryExecuter(self.app.store) self.search.set_query_executer(self.query) self.search.set_result_view(FinancialSearchResults) self.result_view = self.search.result_view self.result_view.page = self tree_view = self.search.result_view.get_treeview() tree_view.set_rules_hint(True) tree_view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH) def _add_date_filter(self): self.date_filter = DateSearchFilter(_('Date:')) self.date_filter.clear_options() self.date_filter.add_option(Any, 0) year = datetime.datetime.today().year month_names = get_month_names() for i, month in enumerate(month_names): name = month_names[i] option = type(name + 'Option', (MonthOption, ), { 'name': _(name), 'month': i + 1, 'year': year }) self.date_filter.add_option(option, i + 1) self.date_filter.add_custom_options() self.date_filter.mode.select_item_by_position(0) self.search.add_filter(self.date_filter) def _append_date_query(self, field): date = self.date_filter.get_state() queries = [] if isinstance(date, DateQueryState) and date.date is not None: queries.append(Date(field) == date.date) elif isinstance(date, DateIntervalQueryState): queries.append(Date(field) >= date.start) queries.append(Date(field) <= date.end) return queries def _payment_query(self, store): queries = self._append_date_query(self.query.table.due_date) if queries: return store.find(self.query.table, And(*queries)) return store.find(self.query.table) def _transaction_query(self, store): queries = [ Or(self.model.id == AccountTransaction.account_id, self.model.id == AccountTransaction.source_account_id) ] queries.extend(self._append_date_query(AccountTransaction.date)) return store.find(AccountTransactionView, And(*queries)) def show(self): self.search.show() def _setup_search(self): if self.model.kind == 'account': self.query.set_table(AccountTransactionView) self.search.set_text_field_columns(['description']) self.query.set_query(self._transaction_query) elif self.model.kind == 'payable': self.search.set_text_field_columns( ['description', 'supplier_name']) self.query.set_table(OutPaymentView) self.query.set_query(self._payment_query) elif self.model.kind == 'receivable': self.search.set_text_field_columns(['description', 'drawee']) self.query.set_table(InPaymentView) self.query.set_query(self._payment_query) else: raise TypeError("unknown model kind: %r" % (self.model.kind, )) def refresh(self): self.search.result_view.clear() if self.model.kind == 'account': transactions = AccountTransactionView.get_for_account( self.model, self.app.store) self.append_transactions(transactions) elif self.model.kind == 'payable': self._populate_payable_payments(OutPaymentView) elif self.model.kind == 'receivable': self._populate_payable_payments(InPaymentView) else: raise TypeError("unknown model kind: %r" % (self.model.kind, )) def _get_columns(self, kind): if kind in ['payable', 'receivable']: return self._get_payment_columns() else: return self._get_account_columns() def _get_account_columns(self): def format_withdrawal(value): if value < 0: return currency(abs(value)).format(symbol=True, precision=2) def format_deposit(value): if value > 0: return currency(value).format(symbol=True, precision=2) if self.model.account_type == Account.TYPE_INCOME: color_func = lambda x: False else: color_func = lambda x: x < 0 return [ Column('date', title=_("Date"), data_type=datetime.date, sorted=True), Column('code', title=_("Code"), data_type=unicode), Column('description', title=_("Description"), data_type=unicode, expand=True), Column('account', title=_("Account"), data_type=unicode), Column('value', title=self.model.account.get_type_label(out=False), data_type=currency, format_func=format_deposit), Column('value', title=self.model.account.get_type_label(out=True), data_type=currency, format_func=format_withdrawal), ColoredColumn('total', title=_("Total"), data_type=currency, color='red', data_func=color_func) ] def _get_payment_columns(self): return [ SearchColumn('due_date', title=_("Due date"), data_type=datetime.date, sorted=True), IdentifierColumn('identifier', title=_("Code")), SearchColumn('description', title=_("Description"), data_type=unicode, expand=True), SearchColumn('value', title=_("Value"), data_type=currency) ] def append_transactions(self, transactions): for transaction in transactions: description = transaction.get_account_description(self.model) value = transaction.get_value(self.model) self._add_transaction(transaction, description, value) self.update_totals() def _populate_payable_payments(self, view_class): for view in self.app.store.find(view_class): self.search.result_view.append(view) def _add_transaction(self, transaction, description, value): item = Settable(transaction=transaction) self._update_transaction(item, transaction, description, value) self.search.result_view.append(item) return item def _update_transaction(self, item, transaction, description, value): item.account = description item.date = transaction.date item.description = transaction.description item.value = value item.code = transaction.code def update_totals(self): total = decimal.Decimal('0') for item in self.search.result_view: total += item.value item.total = total def _edit_transaction_dialog(self, item): store = api.new_store() if isinstance(item.transaction, AccountTransactionView): account_transaction = store.fetch(item.transaction.transaction) else: account_transaction = store.fetch(item.transaction) model = getattr(self.model, 'account', self.model) transaction = run_dialog(AccountTransactionEditor, self.app, store, account_transaction, model) if transaction: store.flush() self._update_transaction(item, transaction, transaction.edited_account.description, transaction.value) self.update_totals() self.search.result_view.update(item) self.app.accounts.refresh_accounts(self.app.store) store.confirm(transaction) store.close() def on_dialog__opened(self, dialog): dialog.connect('account-added', self.on_dialog__account_added) def on_dialog__account_added(self, dialog): self.app.accounts.refresh_accounts(self.app.store) def add_transaction_dialog(self): store = api.new_store() model = getattr(self.model, 'account', self.model) model = store.fetch(model) transaction = run_dialog(AccountTransactionEditor, self.app, store, None, model) if transaction: transaction.sync() value = transaction.value other = transaction.get_other_account(model) if other == model: value = -value item = self._add_transaction(transaction, other.description, value) self.update_totals() self.search.result_view.update(item) self.app.accounts.refresh_accounts(self.app.store) store.confirm(transaction) store.close() def _on_search__item_activated(self, objectlist, item): if self.model.kind == 'account': self._edit_transaction_dialog(item)
class QueryEntryGadget(object): """This gadget modifies a ProxyEntry to behave like a ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a Gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have a button to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) """ MIN_KEY_LENGTH = 1 LOADING_ITEMS_TEXT = _("Loading items...") NEW_ITEM_TEXT = _("Create a new item with that name") NEW_ITEM_TOOLTIP = _("Create a new item") EDIT_ITEM_TOOLTIP = _("Edit the selected item") INFO_ITEM_TOOLTIP = _("See info about the selected item") NO_ITEMS_FOUND_TEXT = _("No items found") advanced_search = True selection_only = False item_editor = None item_info_dialog = ClientEditor search_class = None search_spec = None search_columns = None def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None, edit_button=None, info_button=None, search_clause=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._parent = parent self._on_run_editor = run_editor self._can_edit = False self._search_clause = search_clause self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.edit_button = edit_button self.info_button = info_button self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.search_spec) self._executer.set_filter_columns(self._filter, self.search_columns) self._last_operation = None self._source_id = None self._setup() self.set_value(initial_value, force=True) # # Public API # def set_value(self, obj, force=False): if not force and obj == self._current_obj: return obj = self.store.fetch(obj) if obj is not None: if hasattr(obj, 'description'): value = obj.description else: value = obj.get_description() self.entry.prefill([(value, obj)]) self.update_edit_button(Gtk.STOCK_EDIT, self.EDIT_ITEM_TOOLTIP) else: value = '' self.entry.prefill([]) self.update_edit_button(Gtk.STOCK_NEW, self.NEW_ITEM_TOOLTIP) self._current_obj = obj self.entry.update(obj) self.entry.set_text(value) self._update_widgets() def set_editable(self, can_edit): self.entry.set_property('editable', can_edit) self._update_widgets() def update_edit_button(self, stock, tooltip=None): image = Gtk.Image.new_from_stock(stock, Gtk.IconSize.MENU) self.edit_button.set_image(image) if tooltip is not None: self.edit_button.set_tooltip_text(tooltip) def get_object_from_item(self, item): return item def describe_item(self, item): raise NotImplementedError # # Private # def _setup(self): if not self.selection_only: if self.edit_button is None or self.info_button is None: self._replace_widget() if self.edit_button is None: self.edit_button = self._add_button(Gtk.STOCK_NEW) self.edit_button.connect('clicked', self._on_edit_button__clicked) if self.info_button is None: self.info_button = self._add_button(Gtk.STOCK_INFO) self.info_button.connect('clicked', self._on_info_button__clicked) self.entry.connect('activate', self._on_entry__activate) self.entry.connect('changed', self._on_entry__changed) self.entry.connect('notify::sensitive', self._on_entry_sensitive) self.entry.connect('key-press-event', self._on_entry__key_press_event) self._popup = _QueryEntryPopup( self, has_new_item=not self.selection_only) self._popup.connect('item-selected', self._on_popup__item_selected) self._popup.connect('create-item', self._on_popup__create_item) def _update_widgets(self): self._can_edit = self.entry.get_editable() and self.entry.get_sensitive() if self.edit_button is not None: self.edit_button.set_sensitive(bool(self._can_edit or self._current_obj)) if self.info_button is not None: self.info_button.set_sensitive(bool(self._current_obj)) def _add_button(self, stock): image = Gtk.Image.new_from_stock(stock, Gtk.IconSize.MENU) button = Gtk.Button() button.set_relief(Gtk.ReliefStyle.NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False, 0) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self.entry.props.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in container.__class__.list_child_properties(): props[pspec.name] = container.child_get_property(self.entry, pspec.name) self.box = Gtk.HBox() self.box.show() self.entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _find_items(self, text): self._filter.set_state(text) state = self._filter.get_state() resultset = self._executer.search([state]) if self._search_clause: resultset = resultset.find(self._search_clause) return self._executer.search_async(resultset=resultset, limit=10) def _dispatch(self, value): self._source_id = None if self._last_operation is not None: self._last_operation.cancel() self._last_operation = self._find_items(value) self._last_operation.connect( 'finish', lambda o: self._popup.add_items(o.get_result())) def _run_search(self): if not self.advanced_search: return text = self.entry.get_text() item = run_dialog(self.search_class, self._parent, self.store, double_click_confirm=True, initial_string=text) if item: self.set_value(self.get_object_from_item(item)) def _run_editor(self, model=None, description=None): with api.new_store() as store: model = store.fetch(model) if self._on_run_editor is not None: retval = self._on_run_editor(store, model, description=description, visual_mode=not self._can_edit) else: if issubclass(self.item_editor, BasePersonRoleEditor): rd = run_person_role_dialog else: rd = run_dialog retval = rd(self.item_editor, self._parent, store, model, description=description, visual_mode=not self._can_edit) if store.committed: return self.store.fetch(retval) # # Callbacks # def _on_entry__key_press_event(self, window, event): keyval = event.keyval if keyval == Gdk.KEY_Up or keyval == Gdk.KEY_KP_Up: self._popup.scroll(relative=-1) return True elif keyval == Gdk.KEY_Down or keyval == Gdk.KEY_KP_Down: self._popup.scroll(relative=+1) return True elif keyval == Gdk.KEY_Page_Up: self._popup.scroll(relative=-14) return True elif keyval == Gdk.KEY_Page_Down: self._popup.scroll(relative=+14) return True elif keyval == Gdk.KEY_Escape: self._popup.popdown() return True return False def _on_entry__changed(self, entry): value = unicode(entry.get_text()) self.set_value(None) if len(value) >= self.MIN_KEY_LENGTH: if self._source_id is not None: GLib.source_remove(self._source_id) self._source_id = GLib.timeout_add(150, self._dispatch, value) if not self._popup.visible: self._popup.popup() self._popup.set_loading(True) elif self._popup.visible: # In this case, the user has deleted text to less than the # min key length, so pop it down if self._source_id is not None: GLib.source_remove(self._source_id) self._source_id = None self._popup.popdown() def _on_entry__activate(self, entry): if self._popup.visible: self._popup.popdown() self._popup.confirm() else: self._run_search() def _on_entry_sensitive(self, entry, pspec): self._update_widgets() def _on_popup__item_selected(self, popup, item, fallback_to_search): self.set_value(self.get_object_from_item(item)) popup.popdown() self.entry.grab_focus() GLib.idle_add(self.entry.select_region, len(self.entry.get_text()), -1) if item is None and fallback_to_search: self._run_search() def _on_popup__create_item(self, popup): obj = self._run_editor(description=unicode(self.entry.get_text())) self.set_value(obj) def _on_edit_button__clicked(self, entry): current_obj = self.entry.read() obj = self._run_editor(current_obj) if obj: self.set_value(obj, force=True) def _on_info_button__clicked(self, entry): obj = self.entry.read() with api.new_store() as store: run_dialog(self.item_info_dialog, self._parent, store, store.fetch(obj))
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
class SearchEntryGadget(object): find_tooltip = _('Search') edit_tooltip = _('Edit') new_tooltip = _('Create') def __init__(self, entry, store, model, model_property, search_columns, search_class, parent, run_editor=None): """ This gadget modifies a ProxyEntry turning it into a replacement for ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have two buttons: One for showing the related search dialog (or search editor), and another one to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) :param entry: The entry that we should modify :param store: The store that will be used for database queries :param model: The model that we are updating :param model_property: Property name of the model that should be updated :param search_columns: Columns that will be queried when the user activates the entry :param search_class: Class of the search editor/dialog that will be displayed when more than one object is found :param parent: The parent that should be respected when running other dialogs :param find_tooltip: the tooltip to use for the search button :param edit_tooltip: the tooltip to use for the edit button :param new_tooltip: the tooltip to use for the new button """ self.store = store self._entry = entry self._model = model # TODO: Maybe this two variables shoulb be a list of properties of the # table instead of strings self._model_property = model_property self._search_columns = search_columns self._search_class = search_class self._parent = parent self._on_run_editor = run_editor # TODO: Respect permission manager self._editor_class = search_class.editor_class # If the search is for a person, the editor is called with a special # function if issubclass(search_class, BasePersonSearch): self._is_person = True else: self._is_person = False self._setup_widgets() self._setup_callbacks() # # Private API # def _setup_widgets(self): self._replace_widget() # Add the two buttons self.find_button = self._create_button(gtk.STOCK_FIND) self.edit_button = self._create_button(gtk.STOCK_NEW) can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.find_button.set_tooltip_text(self.find_tooltip) self.edit_button.set_tooltip_text(self.new_tooltip) # the entry needs a completion to work in MODE_DATA self._completion = gtk.EntryCompletion() self._entry.set_completion(self._completion) self._entry.set_mode(ENTRY_MODE_DATA) initial_value = getattr(self._model, self._model_property) self.set_value(initial_value) # The filter that will be used. This is not really in the interface. We # will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self._search_class.search_spec) self._executer.set_filter_columns(self._filter, self._search_columns) def _create_button(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self._entry.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in gtk.container_class_list_child_properties(container): props[pspec.name] = container.child_get_property( self._entry, pspec.name) self.box = gtk.HBox() self.box.show() self._entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _setup_callbacks(self): self._entry.connect('activate', self._on_entry_activate) self._entry.connect('changed', self._on_entry_changed) self._entry.connect('notify::sensitive', self._on_entry_sensitive) self.find_button.connect('clicked', self._on_find_button__clicked) self.edit_button.connect('clicked', self._on_edit_button__clicked) def _run_search(self): text = self._entry.get_text() value = run_dialog(self._search_class, self._parent, self.store, double_click_confirm=True, initial_string=text) if value: self.set_value(self.get_model_obj(value)) def _run_editor(self): with api.new_store() as store: model = getattr(self._model, self._model_property) model = store.fetch(model) if self._on_run_editor: value = self._on_run_editor(store, model) elif self._is_person: value = run_person_role_dialog(self._editor_class, self._parent, store, model) else: value = run_dialog(self._editor_class, self._parent, store, model) if value: value = self.store.fetch(self.get_model_obj(value)) self.set_value(value) # # Public API # def set_value(self, obj): if obj: display_value = obj.get_description() self._entry.prefill([(display_value, obj)]) self.update_edit_button(gtk.STOCK_INFO, self.edit_tooltip) else: display_value = '' self._entry.prefill([]) self.update_edit_button(gtk.STOCK_NEW, self.new_tooltip) self._value = obj self._entry.update(obj) self._entry.set_text(display_value) def get_model_obj(self, obj): return obj def update_edit_button(self, stock, tooltip): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) self.edit_button.set_image(image) self.edit_button.set_tooltip_text(tooltip) # # Callbacks # def _on_entry_activate(self, entry): if not self._entry.get_property('editable'): return text = entry.get_text() self._filter.set_state(text) state = self._filter.get_state() results = list(self._executer.search([state])[:2]) if len(results) != 1: # XXX: If nothing is found in the query above, runing the search # will cause the query to be executed a second time. Refactor the # search to allow us to send the initial results avoiding this # second query. return self._run_search() # This means the search returned only one result. self.set_value(self.get_model_obj(results[0])) def _on_entry_changed(self, entry): # If the user edits the value in the entry, it invalidates the value. if self._value: self.set_value(None) def _on_entry_sensitive(self, entry, pspec): can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.edit_button.set_sensitive(can_edit) def _on_edit_button__clicked(self, entry): self._run_editor() def _on_find_button__clicked(self, entry): self._run_search()
class LoanSelectionStep(BaseWizardStep): gladefile = 'HolderTemplate' def __init__(self, wizard, store): BaseWizardStep.__init__(self, store, wizard) self.setup_slaves() def _create_filters(self): self.search.set_text_field_columns(['client_name']) def _get_columns(self): return [IdentifierColumn('identifier', sorted=True), SearchColumn('responsible_name', title=_(u'Responsible'), data_type=str, expand=True), SearchColumn('client_name', title=_(u'Client'), data_type=str, expand=True), SearchColumn('open_date', title=_(u'Opened'), data_type=datetime.date), SearchColumn('expire_date', title=_(u'Expire'), data_type=datetime.date), Column('loaned', title=_(u'Loaned'), data_type=Decimal), ] def _refresh_next(self, value=None): can_continue = False selected_rows = self.search.results.get_selected_rows() if selected_rows: client = selected_rows[0].client_id branch = selected_rows[0].branch_id # Only loans that belong to the same client and are from the same # branch can be closed together can_continue = all(v.client_id == client and v.branch_id == branch for v in selected_rows) self.wizard.refresh_next(can_continue) def get_extra_query(self, states): return LoanView.status == Loan.STATUS_OPEN def setup_slaves(self): self.search = SearchSlave(self._get_columns(), restore_name=self.__class__.__name__) self.search.enable_advanced_search() self.attach_slave('place_holder', self.search) self.executer = QueryExecuter(self.store) self.search.set_query_executer(self.executer) self.executer.set_table(LoanView) self.executer.add_query_callback(self.get_extra_query) self._create_filters() self.search.results.connect('selection-changed', self._on_results_selection_changed) self.search.results.set_selection_mode(gtk.SELECTION_MULTIPLE) self.search.focus_search_entry() # # WizardStep # def has_previous_step(self): return False def post_init(self): self.register_validate_function(self._refresh_next) self.force_validation() def next_step(self): # FIXME: For some reason, the loan isn't in self.store views = self.search.results.get_selected_rows() self.wizard.models = [self.store.fetch(v.loan) for v in views] return LoanItemSelectionStep(self.wizard, self, self.store, self.wizard.models) # # Callbacks # def _on_results_selection_changed(self, widget, selection): self._refresh_next()
class SearchEntryGadget(object): find_tooltip = _('Search') edit_tooltip = _('Edit') new_tooltip = _('Create') def __init__(self, entry, store, model, model_property, search_columns, search_class, parent): """ This gadget modifies a ProxyEntry turning it into a replacement for ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have two buttons: One for showing the related search dialog (or search editor), and another one to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) :param entry: The entry that we should modify :param store: The store that will be used for database queries :param model: The model that we are updating :param model_property: Property name of the model that should be updated :param search_columns: Columns that will be queried when the user activates the entry :param search_class: Class of the search editor/dialog that will be displayed when more than one object is found :param parent: The parent that should be respected when running other dialogs :param find_tooltip: the tooltip to use for the search button :param edit_tooltip: the tooltip to use for the edit button :param new_tooltip: the tooltip to use for the new button """ self.store = store self._entry = entry self._model = model # TODO: Maybe this two variables shoulb be a list of properties of the # table instead of strings self._model_property = model_property self._search_columns = search_columns self._search_class = search_class self._parent = parent # TODO: Respect permission manager self._editor_class = search_class.editor_class # If the search is for a person, the editor is called with a special # function if issubclass(search_class, BasePersonSearch): self._is_person = True else: self._is_person = False self._setup_widgets() self._setup_callbacks() # # Private API # def _setup_widgets(self): self._replace_widget() # Add the two buttons self.find_button = self._create_button(gtk.STOCK_FIND) self.edit_button = self._create_button(gtk.STOCK_NEW) can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.find_button.set_tooltip_text(self.find_tooltip) self.edit_button.set_tooltip_text(self.new_tooltip) # the entry needs a completion to work in MODE_DATA self._completion = gtk.EntryCompletion() self._entry.set_completion(self._completion) self._entry.set_mode(ENTRY_MODE_DATA) initial_value = getattr(self._model, self._model_property) self.set_value(initial_value) # The filter that will be used. This is not really in the interface. We # will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self._search_class.search_spec) self._executer.set_filter_columns(self._filter, self._search_columns) def _create_button(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self._entry.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in gtk.container_class_list_child_properties(container): props[pspec.name] = container.child_get_property(self._entry, pspec.name) self.box = gtk.HBox() self.box.show() self._entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _setup_callbacks(self): self._entry.connect('activate', self._on_entry_activate) self._entry.connect('changed', self._on_entry_changed) self._entry.connect('notify::sensitive', self._on_entry_sensitive) self.find_button.connect('clicked', self._on_find_button__clicked) self.edit_button.connect('clicked', self._on_edit_button__clicked) def _run_search(self): text = self._entry.get_text() value = run_dialog(self._search_class, self._parent, self.store, double_click_confirm=True, initial_string=text) if value: self.set_value(value) def _run_editor(self): with api.new_store() as store: model = getattr(self._model, self._model_property) model = store.fetch(model) if self._is_person: value = run_person_role_dialog(self._editor_class, self._parent, store, model) else: value = run_dialog(self._editor_class, self._parent, store, model) if value: value = self.store.fetch(value) self.set_value(value) # # Public API # def set_value(self, obj): if obj: display_value = obj.get_description() obj_id = obj.id self._entry.prefill([(display_value, obj_id)]) self.set_edit_button_stock(gtk.STOCK_INFO) self.edit_button.set_tooltip_text(self.edit_tooltip) else: display_value = '' obj_id = None self._entry.prefill([]) self.set_edit_button_stock(gtk.STOCK_NEW) self.edit_button.set_tooltip_text(self.new_tooltip) self._value = obj self._entry.update(obj_id) self._entry.set_text(display_value) def set_edit_button_stock(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) self.edit_button.set_image(image) # # Callbacks # def _on_entry_activate(self, entry): if not self._entry.get_property('editable'): return text = entry.get_text() self._filter.set_state(text) state = self._filter.get_state() results = list(self._executer.search([state])[:2]) if len(results) != 1: # XXX: If nothing is found in the query above, runing the search # will cause the query to be executed a second time. Refactor the # search to allow us to send the initial results avoiding this # second query. return self._run_search() # This means the search returned only one result. self.set_value(results[0]) def _on_entry_changed(self, entry): # If the user edits the value in the entry, it invalidates the value. if self._value: self.set_value(None) def _on_entry_sensitive(self, entry, pspec): can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.edit_button.set_sensitive(can_edit) def _on_edit_button__clicked(self, entry): self._run_editor() def _on_find_button__clicked(self, entry): self._run_search()
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()
def _date_filter_query(self, search_spec, column): executer = QueryExecuter(self.store) executer.set_filter_columns(self.date_filter, [column]) executer.set_table(search_spec) return executer.search([self.date_filter.get_state()])
class QueryExecuterTest(DomainTest): def setUp(self): DomainTest.setUp(self) self.qe = QueryExecuter(self.store) self.qe.set_search_spec(ClientCategory) self.sfilter = mock.Mock() self.qe.set_filter_columns(self.sfilter, ['name']) def _search_async(self, states): op = self.qe.search_async(states) self.qe._operation_executer._queue.join() return list(op.get_result()) def _search_string_all(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_ALL, text=text) ]) def _search_string_all_async(self, text): return self._search_async([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_ALL, text=text) ]) def _search_string_exactly(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_EXACTLY, text=text) ]) def _search_string_exactly_async(self, text): return self._search_async([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_EXACTLY, text=text) ]) def _search_string_not(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.NOT_CONTAINS, text=text) ]) def _search_string_not_async(self, text): return self._search_async([ StringQueryState(filter=self.sfilter, mode=StringQueryState.NOT_CONTAINS, text=text) ]) def test_string_query(self): self.assertEqual(self.store.find(ClientCategory).count(), 0) self.create_client_category(u'EYE MOON FLARE 110 0.5') self.create_client_category(u'EYE MOON FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 110 1.0') self.create_client_category(u'EYE SUN STONE 120 0.5') self.assertEqual(self._search_string_all(u'eye flare 110').count(), 2) self.assertEqual(self._search_string_all(u'eye 0.5').count(), 2) self.assertEqual(self._search_string_all(u'eye 120').count(), 3) self.assertEqual( self._search_string_exactly(u'eye flare 110').count(), 0) self.assertEqual(self._search_string_exactly(u'eye 0.5').count(), 0) self.assertEqual(self._search_string_exactly(u'eye 120').count(), 0) self.assertEqual(self._search_string_not(u'stone 110').count(), 2) self.assertEqual(self._search_string_not(u'eye').count(), 0) self.assertEqual(self._search_string_not(u'moon 120').count(), 1) def test_search_async(self): self.assertEqual(self.store.find(ClientCategory).count(), 0) try: self.create_client_category(u'EYE MOON FLARE 110 0.5') self.create_client_category(u'EYE MOON FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 110 1.0') self.create_client_category(u'EYE SUN STONE 120 0.5') # search_async uses another connection. Because of that, we need to # commit the store or else it will not be able to find the objects self.store.commit() self.assertEqual( len(self._search_string_all_async(u'eye flare 110')), 2) self.assertEqual(len(self._search_string_all_async(u'eye 0.5')), 2) self.assertEqual(len(self._search_string_all_async(u'eye 120')), 3) self.assertEqual( len(self._search_string_exactly_async(u'eye flare 110')), 0) self.assertEqual( len(self._search_string_exactly_async(u'eye 0.5')), 0) self.assertEqual( len(self._search_string_exactly_async(u'eye 120')), 0) self.assertEqual(len(self._search_string_not_async(u'stone 110')), 2) self.assertEqual(len(self._search_string_not_async(u'eye')), 0) self.assertEqual(len(self._search_string_not_async(u'moon 120')), 1) finally: self.clean_domain([ClientCategory]) self.store.commit()
class TransactionPage(object): # shows either a list of: # - transactions # - payments def __init__(self, model, app, parent): self.model = model self.app = app self.parent_window = parent self._block = False self._create_search() self._add_date_filter() self._setup_search() self.refresh() def get_toplevel(self): return self.parent_window def _create_search(self): self.search = SearchSlave(self._get_columns(self.model.kind)) self.search.connect('result-item-activated', self._on_search__item_activated) self.search.enable_advanced_search() self.query = QueryExecuter(self.app.store) self.search.set_query_executer(self.query) self.search.set_result_view(FinancialSearchResults) self.result_view = self.search.result_view self.result_view.page = self tree_view = self.search.result_view.get_treeview() tree_view.set_rules_hint(True) tree_view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH) def _add_date_filter(self): self.date_filter = DateSearchFilter(_('Date:')) self.date_filter.clear_options() self.date_filter.add_option(Any, 0) year = datetime.datetime.today().year month_names = get_month_names() for i, month in enumerate(month_names): name = month_names[i] option = type(name + 'Option', (MonthOption, ), {'name': _(name), 'month': i + 1, 'year': year}) self.date_filter.add_option(option, i + 1) self.date_filter.add_custom_options() self.date_filter.mode.select_item_by_position(0) self.search.add_filter(self.date_filter) def _append_date_query(self, field): date = self.date_filter.get_state() queries = [] if isinstance(date, DateQueryState) and date.date is not None: queries.append(Date(field) == date.date) elif isinstance(date, DateIntervalQueryState): queries.append(Date(field) >= date.start) queries.append(Date(field) <= date.end) return queries def _payment_query(self, store): queries = self._append_date_query(self.query.table.due_date) if queries: return store.find(self.query.table, And(*queries)) return store.find(self.query.table) def _transaction_query(self, store): queries = [Or(self.model.id == AccountTransaction.account_id, self.model.id == AccountTransaction.source_account_id)] queries.extend(self._append_date_query(AccountTransaction.date)) return store.find(AccountTransactionView, And(*queries)) def show(self): self.search.show() def _setup_search(self): if self.model.kind == 'account': self.query.set_table(AccountTransactionView) self.search.set_text_field_columns(['description']) self.query.set_query(self._transaction_query) elif self.model.kind == 'payable': self.search.set_text_field_columns(['description', 'supplier_name']) self.query.set_table(OutPaymentView) self.query.set_query(self._payment_query) elif self.model.kind == 'receivable': self.search.set_text_field_columns(['description', 'drawee']) self.query.set_table(InPaymentView) self.query.set_query(self._payment_query) else: raise TypeError("unknown model kind: %r" % (self.model.kind, )) def refresh(self): self.search.result_view.clear() if self.model.kind == 'account': transactions = AccountTransactionView.get_for_account(self.model, self.app.store) self.append_transactions(transactions) elif self.model.kind == 'payable': self._populate_payable_payments(OutPaymentView) elif self.model.kind == 'receivable': self._populate_payable_payments(InPaymentView) else: raise TypeError("unknown model kind: %r" % (self.model.kind, )) def _get_columns(self, kind): if kind in ['payable', 'receivable']: return self._get_payment_columns() else: return self._get_account_columns() def _get_account_columns(self): def format_withdrawal(value): if value < 0: return currency(abs(value)).format(symbol=True, precision=2) def format_deposit(value): if value > 0: return currency(value).format(symbol=True, precision=2) if self.model.account_type == Account.TYPE_INCOME: color_func = lambda x: False else: color_func = lambda x: x < 0 return [Column('date', title=_("Date"), data_type=datetime.date, sorted=True), Column('code', title=_("Code"), data_type=unicode), Column('description', title=_("Description"), data_type=unicode, expand=True), Column('account', title=_("Account"), data_type=unicode), Column('value', title=self.model.account.get_type_label(out=False), data_type=currency, format_func=format_deposit), Column('value', title=self.model.account.get_type_label(out=True), data_type=currency, format_func=format_withdrawal), ColoredColumn('total', title=_("Total"), data_type=currency, color='red', data_func=color_func)] def _get_payment_columns(self): return [SearchColumn('due_date', title=_("Due date"), data_type=datetime.date, sorted=True), IdentifierColumn('identifier', title=_("Code")), SearchColumn('description', title=_("Description"), data_type=unicode, expand=True), SearchColumn('value', title=_("Value"), data_type=currency)] def append_transactions(self, transactions): for transaction in transactions: description = transaction.get_account_description(self.model) value = transaction.get_value(self.model) self._add_transaction(transaction, description, value) self.update_totals() def _populate_payable_payments(self, view_class): for view in self.app.store.find(view_class): self.search.result_view.append(view) def _add_transaction(self, transaction, description, value): item = Settable(transaction=transaction) self._update_transaction(item, transaction, description, value) self.search.result_view.append(item) return item def _update_transaction(self, item, transaction, description, value): item.account = description item.date = transaction.date item.description = transaction.description item.value = value item.code = transaction.code def update_totals(self): total = decimal.Decimal('0') for item in self.search.result_view: total += item.value item.total = total def _edit_transaction_dialog(self, item): store = api.new_store() if isinstance(item.transaction, AccountTransactionView): account_transaction = store.fetch(item.transaction.transaction) else: account_transaction = store.fetch(item.transaction) model = getattr(self.model, 'account', self.model) transaction = run_dialog(AccountTransactionEditor, self.app, store, account_transaction, model) if transaction: store.flush() self._update_transaction(item, transaction, transaction.edited_account.description, transaction.value) self.update_totals() self.search.result_view.update(item) self.app.accounts.refresh_accounts(self.app.store) store.confirm(transaction) store.close() def on_dialog__opened(self, dialog): dialog.connect('account-added', self.on_dialog__account_added) def on_dialog__account_added(self, dialog): self.app.accounts.refresh_accounts(self.app.store) def add_transaction_dialog(self): store = api.new_store() model = getattr(self.model, 'account', self.model) model = store.fetch(model) transaction = run_dialog(AccountTransactionEditor, self.app, store, None, model) if transaction: transaction.sync() value = transaction.value other = transaction.get_other_account(model) if other == model: value = -value item = self._add_transaction(transaction, other.description, value) self.update_totals() self.search.result_view.update(item) self.app.accounts.refresh_accounts(self.app.store) store.confirm(transaction) store.close() def _on_search__item_activated(self, objectlist, item): if self.model.kind == 'account': self._edit_transaction_dialog(item)
class QueryExecuterTest(DomainTest): def setUp(self): DomainTest.setUp(self) self.qe = QueryExecuter(self.store) self.qe.set_search_spec(ClientCategory) self.sfilter = mock.Mock() self.qe.set_filter_columns(self.sfilter, ['name']) def _search_async(self, states): op = self.qe.search_async(states) self.qe._operation_executer._queue.join() return list(op.get_result()) def _search_string_all(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_ALL, text=text)]) def _search_string_all_async(self, text): return self._search_async([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_ALL, text=text)]) def _search_string_exactly(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_EXACTLY, text=text)]) def _search_string_exactly_async(self, text): return self._search_async([ StringQueryState(filter=self.sfilter, mode=StringQueryState.CONTAINS_EXACTLY, text=text)]) def _search_string_not(self, text): return self.qe.search([ StringQueryState(filter=self.sfilter, mode=StringQueryState.NOT_CONTAINS, text=text)]) def _search_string_not_async(self, text): return self._search_async([ StringQueryState(filter=self.sfilter, mode=StringQueryState.NOT_CONTAINS, text=text)]) def test_string_query(self): self.assertEquals(self.store.find(ClientCategory).count(), 0) self.create_client_category(u'EYE MOON FLARE 110 0.5') self.create_client_category(u'EYE MOON FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 110 1.0') self.create_client_category(u'EYE SUN STONE 120 0.5') self.assertEquals(self._search_string_all(u'eye flare 110').count(), 2) self.assertEquals(self._search_string_all(u'eye 0.5').count(), 2) self.assertEquals(self._search_string_all(u'eye 120').count(), 3) self.assertEquals(self._search_string_exactly(u'eye flare 110').count(), 0) self.assertEquals(self._search_string_exactly(u'eye 0.5').count(), 0) self.assertEquals(self._search_string_exactly(u'eye 120').count(), 0) self.assertEquals(self._search_string_not(u'stone 110').count(), 2) self.assertEquals(self._search_string_not(u'eye').count(), 0) self.assertEquals(self._search_string_not(u'moon 120').count(), 1) def test_search_async(self): self.assertEquals(self.store.find(ClientCategory).count(), 0) try: self.create_client_category(u'EYE MOON FLARE 110 0.5') self.create_client_category(u'EYE MOON FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 120 1.0') self.create_client_category(u'EYE SUN FLARE 110 1.0') self.create_client_category(u'EYE SUN STONE 120 0.5') # search_async uses another connection. Because of that, we need to # commit the store or else it will not be able to find the objects self.store.commit() self.assertEquals( len(self._search_string_all_async(u'eye flare 110')), 2) self.assertEquals( len(self._search_string_all_async(u'eye 0.5')), 2) self.assertEquals( len(self._search_string_all_async(u'eye 120')), 3) self.assertEquals( len(self._search_string_exactly_async(u'eye flare 110')), 0) self.assertEquals( len(self._search_string_exactly_async(u'eye 0.5')), 0) self.assertEquals( len(self._search_string_exactly_async(u'eye 120')), 0) self.assertEquals( len(self._search_string_not_async(u'stone 110')), 2) self.assertEquals( len(self._search_string_not_async(u'eye')), 0) self.assertEquals( len(self._search_string_not_async(u'moon 120')), 1) finally: self.clean_domain([ClientCategory]) self.store.commit()
class QueryEntryGadget(object): """This gadget modifies a ProxyEntry to behave like a ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have a button to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) """ MIN_KEY_LENGTH = 1 LOADING_ITEMS_TEXT = _("Loading items...") NEW_ITEM_TEXT = _("Create a new item with that name") NEW_ITEM_TOOLTIP = _("Create a new item") EDIT_ITEM_TOOLTIP = _("Edit the selected item") ITEM_EDITOR = None SEARCH_CLASS = None SEARCH_SPEC = None SEARCH_COLUMNS = None def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._current_obj = None self._parent = parent self._on_run_editor = run_editor self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.SEARCH_SPEC) self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS) self._last_operation = None self._source_id = None self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor) self._setup() self.set_value(initial_value, force=True) # # Public API # def set_value(self, obj, force=False): if not force and obj == self._current_obj: return obj = self.store.fetch(obj) if obj is not None: value = obj.get_description() self.entry.prefill([(value, obj)]) self.update_edit_button(gtk.STOCK_INFO, self.EDIT_ITEM_TOOLTIP) else: value = '' self.entry.prefill([]) self.update_edit_button(gtk.STOCK_NEW, self.NEW_ITEM_TOOLTIP) self._current_obj = obj self.entry.update(obj) self.entry.set_text(value) def set_editable(self, can_edit): self.edit_button.set_sensitive(can_edit) self.entry.set_property('editable', can_edit) def update_edit_button(self, stock, tooltip): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) self.edit_button.set_image(image) self.edit_button.set_tooltip_text(tooltip) def get_object_from_item(self, item): return item def describe_item(self, item): raise NotImplementedError # # Private # def _setup(self): self._replace_widget() self.edit_button = self._add_button(gtk.STOCK_NEW) self.edit_button.connect('clicked', self._on_edit_button__clicked) self.entry.connect('activate', self._on_entry__activate) self.entry.connect('changed', self._on_entry__changed) self.entry.connect('notify::sensitive', self._on_entry_sensitive) self.entry.connect('key-press-event', self._on_entry__key_press_event) self._popup = _QueryEntryPopup(self) self._popup.connect('item-selected', self._on_popup__item_selected) self._popup.connect('create-item', self._on_popup__create_item) def _add_button(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self.entry.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in gtk.container_class_list_child_properties(container): props[pspec.name] = container.child_get_property(self.entry, pspec.name) self.box = gtk.HBox() self.box.show() self.entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _find_items(self, text): self._filter.set_state(text) state = self._filter.get_state() return self._executer.search_async([state], limit=10) def _dispatch(self, value): self._source_id = None if self._last_operation is not None: self._last_operation.cancel() self._last_operation = self._find_items(value) self._last_operation.connect( 'finish', lambda o: self._popup.add_items(o.get_result())) def _run_search(self): text = self.entry.get_text() if not text: return item = run_dialog(self.SEARCH_CLASS, self._parent, self.store, double_click_confirm=True, initial_string=text) if item: self.set_value(self.get_object_from_item(item)) def _run_editor(self, model=None, description=None): with api.new_store() as store: model = store.fetch(model) if self._on_run_editor is not None: retval = self._on_run_editor(store, model, description=description) else: rd = run_person_role_dialog if self._is_person else run_dialog retval = rd(self.ITEM_EDITOR, self._parent, store, model, description=description) if store.committed: return self.store.fetch(retval) # # Callbacks # def _on_entry__key_press_event(self, window, event): keyval = event.keyval if keyval == gtk.keysyms.Up or keyval == gtk.keysyms.KP_Up: self._popup.scroll(relative=-1) return True elif keyval == gtk.keysyms.Down or keyval == gtk.keysyms.KP_Down: self._popup.scroll(relative=+1) return True elif keyval == gtk.keysyms.Page_Up: self._popup.scroll(relative=-14) return True elif keyval == gtk.keysyms.Page_Down: self._popup.scroll(relative=+14) return True elif keyval == gtk.keysyms.Escape: self._popup.popdown() return True return False def _on_entry__changed(self, entry): value = unicode(entry.get_text()) self.set_value(None) if len(value) >= self.MIN_KEY_LENGTH: if self._source_id is not None: glib.source_remove(self._source_id) self._source_id = glib.timeout_add(150, self._dispatch, value) if not self._popup.visible: self._popup.popup() self._popup.set_loading(True) elif self._popup.visible: # In this case, the user has deleted text to less than the # min key length, so pop it down if self._source_id is not None: glib.source_remove(self._source_id) self._source_id = None self._popup.popdown() def _on_entry__activate(self, entry): self._popup.popdown() self._popup.confirm() def _on_entry_sensitive(self, entry, pspec): can_edit = entry.get_editable() and entry.get_sensitive() self.edit_button.set_sensitive(can_edit) def _on_popup__item_selected(self, popup, item, fallback_to_search): self.set_value(self.get_object_from_item(item)) popup.popdown() self.entry.grab_focus() glib.idle_add(self.entry.select_region, len(self.entry.get_text()), -1) if item is None and fallback_to_search: self._run_search() def _on_popup__create_item(self, popup): obj = self._run_editor(description=unicode(self.entry.get_text())) self.set_value(obj) def _on_edit_button__clicked(self, entry): current_obj = self.entry.read() obj = self._run_editor(current_obj) if obj: self.set_value(obj, force=True)