[POS-commit] r6396 - in kiwi/trunk/kiwi: . db ui
Johan Dahlin
jdahlin at async.com.br
Thu Apr 19 18:44:33 BRT 2007
Author: jdahlin
Date: Thu Apr 19 18:44:33 2007
New Revision: 6396
Added:
kiwi/trunk/kiwi/db/
kiwi/trunk/kiwi/db/__init__.py
kiwi/trunk/kiwi/db/query.py
kiwi/trunk/kiwi/db/sqlobj.py
kiwi/trunk/kiwi/ui/search.py
Modified:
kiwi/trunk/kiwi/enums.py
kiwi/trunk/kiwi/interfaces.py
Log:
* README: Mention that SQLObject is an optional dependency
* kiwi/db/__init__.py:
* kiwi/db/query.py:
* kiwi/db/sqlobj.py:
* kiwi/enums.py:
* kiwi/interfaces.py:
* kiwi/ui/search.py:
Add a SearchContainer plus database independent infrastructure to
construct queries. Include a SQLObject plugin
Added: kiwi/trunk/kiwi/db/__init__.py
==============================================================================
--- (empty file)
+++ kiwi/trunk/kiwi/db/__init__.py Thu Apr 19 18:44:33 2007
@@ -0,0 +1,22 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2007 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+"""Database integration"""
Added: kiwi/trunk/kiwi/db/query.py
==============================================================================
--- (empty file)
+++ kiwi/trunk/kiwi/db/query.py Thu Apr 19 18:44:33 2007
@@ -0,0 +1,98 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2007 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin at async.com.br>
+#
+
+from kiwi.interfaces import ISearchFilter
+
+#
+# Query building
+#
+
+class QueryState(object):
+ def __init__(self, search_filter):
+ """
+ @param search_filter: search filter this query state is associated with
+ @type search_filter: L{SearchFilter}
+ """
+ self.filter = search_filter
+
+class NumberQueryState(QueryState):
+ """
+ @cvar value: number
+ """
+ def __init__(self, filter, value):
+ QueryState.__init__(self, filter)
+ self.value = value
+
+class StringQueryState(QueryState):
+ """
+ @cvar text: string
+ """
+ def __init__(self, filter, text):
+ QueryState.__init__(self, filter)
+ self.text = text
+
+class DateQueryState(QueryState):
+ """
+ @cvar date: date
+ """
+ def __init__(self, filter, date):
+ QueryState.__init__(self, filter)
+ self.date = date
+
+class DateIntervalQueryState(QueryState):
+ """
+ @cvar start: start of interval
+ @cvar end: end of interval
+ """
+ def __init__(self, filter, start, end):
+ QueryState.__init__(self, filter)
+ self.start = start
+ self.end = end
+
+class QueryExecuter(object):
+ def __init__(self):
+ self._columns = {}
+
+ #
+ # Public API
+ #
+
+ def set_filter_columns(self, search_filter, columns):
+ if not ISearchFilter.providedBy(search_filter):
+ raise TypeError("search_filter must implement ISearchFilter")
+
+ assert not search_filter in self._columns
+ self._columns[search_filter] = columns
+
+ #
+ # Overridable
+ #
+
+ def search(self, states):
+ """
+ @param states:
+ @type states: list of L{QueryStates}
+ @returns: list of objects matching query
+ """
+ raise NotImplementedError
+
Added: kiwi/trunk/kiwi/db/sqlobj.py
==============================================================================
--- (empty file)
+++ kiwi/trunk/kiwi/db/sqlobj.py Thu Apr 19 18:44:33 2007
@@ -0,0 +1,170 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+
+##
+## Copyright (C) 2007 Async Open Source
+##
+## This program is free software; you can redistribute it and/or
+## modify it under the terms of the GNU Lesser General Public License
+## as published by the Free Software Foundation; either version 2
+## of the License, or (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., or visit: http://www.gnu.org/.
+##
+##
+## Author(s): Johan Dahlin <jdahlin at async.com.br>
+##
+
+"""
+SQLObject integration for Kiwi
+"""
+
+from sqlobject.sqlbuilder import func, AND, OR, LIKE
+
+from kiwi.db.query import (NumberQueryState, StringQueryState,
+ DateQueryState, DateIntervalQueryState,
+ QueryExecuter)
+from kiwi.interfaces import ISearchFilter
+
+class SQLObjectQueryExecuter(QueryExecuter):
+ def __init__(self, conn):
+ QueryExecuter.__init__(self)
+ self.conn = conn
+ self.table = None
+ self._filter_query_callbacks = {}
+ self._query_callbacks = []
+ self._query = self._default_query
+
+ #
+ # Public API
+ #
+
+ def set_table(self, table):
+ self.table = table
+
+ def add_filter_query_callback(self, search_filter, callback):
+ """
+ @param search_filter:
+ @param callback:
+ """
+ if not ISearchFilter.providedBy(search_filter):
+ raise TypeError
+ if not callable(callback):
+ raise TypeError
+ l = self._filter_query_callbacks.setdefault(search_filter, [])
+ l.append(callback)
+
+ def add_query_callback(self, callback):
+ """
+ @param callback:
+ """
+ if not callable(callback):
+ raise TypeError
+ self._query_callbacks.append(callback)
+
+ def set_query(self, callback):
+ if callback is None:
+ callback = self._default_query
+ elif not callable(callback):
+ raise TypeError
+
+ self._query = callback
+
+ #
+ # QueryBuilder
+ #
+
+ def search(self, states):
+ """
+ @param states:
+ """
+ if self.table is None:
+ raise ValueError("table cannot be None")
+ table = self.table
+ queries = []
+ for state in states:
+ search_filter = state.filter
+ assert state.filter
+
+ # Column query
+ if search_filter in self._columns:
+ query = self._construct_state_query(
+ table, state, self._columns[search_filter])
+ if query:
+ queries.append(query)
+ # Custom per filter/state query.
+ elif search_filter in self._filter_query_callbacks:
+ for callback in self._filter_query_callbacks[search_filter]:
+ query = callback(state)
+ if query:
+ queries.append(query)
+ elif self._query != self._default_query:
+ continue
+ else:
+ raise ValueError(
+ "You need to add a search column or a query callback "
+ "for filter %s" % (search_filter))
+
+ # Custom query
+ for callback in self._query_callbacks:
+ query = callback()
+ if query:
+ queries.append(query)
+
+ return self._query(AND(*queries), self.conn)
+
+ def _default_query(self, query, conn):
+ return self.table.select(query, connection=conn)
+
+ #
+ # Private
+ #
+
+ def _construct_state_query(self, table, state, columns):
+ queries = []
+ for column in columns:
+ query = None
+ table_field = getattr(table.q, column)
+ if isinstance(state, NumberQueryState):
+ query = self._parse_number_state(state, table_field)
+ elif isinstance(state, StringQueryState):
+ query = self._parse_string_state(state, table_field)
+ elif isinstance(state, DateQueryState):
+ query = self._parse_date_state(state, table_field)
+ elif isinstance(state, DateIntervalQueryState):
+ query = self._parse_date_interval_state(state, table_field)
+ else:
+ raise NotImplementedError(state.__class__.__name__)
+
+ if query:
+ queries.append(query)
+
+ return OR(*queries)
+
+ def _parse_number_state(self, state, table_field):
+ if state.value is not None:
+ return table_field == state.value
+
+ def _parse_string_state(self, state, table_field):
+ if state.text:
+ text = '%%%s%%' % state.text.lower()
+ return LIKE(func.LOWER(table_field), text)
+
+ def _parse_date_state(self, state, table_field):
+ if state.date:
+ return func.DATE(table_field) == func.DATE(state.date)
+
+ def _parse_date_interval_state(self, state, table_field):
+ queries = []
+ if state.start:
+ queries.append(table_field >= func.DATE(state.start))
+ if state.end:
+ queries.append(table_field <= func.DATE(state.end))
+ return AND(*queries)
Modified: kiwi/trunk/kiwi/enums.py
==============================================================================
--- kiwi/trunk/kiwi/enums.py (original)
+++ kiwi/trunk/kiwi/enums.py Thu Apr 19 18:44:33 2007
@@ -50,3 +50,15 @@
READONLY,
UNREMOVABLE,
UNEDITABLE) = range(4)
+
+class SearchFilterPosition(enum):
+ """
+ An enum used to indicate where a search filter should be added to
+ a SearchContainer.
+
+ - TOP: top left corner
+ - BOTTOM: bottom
+ """
+ (TOP,
+ BOTTOM) = range(2)
+
Modified: kiwi/trunk/kiwi/interfaces.py
==============================================================================
--- kiwi/trunk/kiwi/interfaces.py (original)
+++ kiwi/trunk/kiwi/interfaces.py Thu Apr 19 18:44:33 2007
@@ -167,3 +167,22 @@
"""Connect the signals in the keys of dict with the objects in the
values of dic
"""
+
+class ISearchFilter(Interface):
+
+ def get_widget():
+ """
+ @rtype: L{gtk.Widget} subclass
+ """
+
+ def get_state():
+ """
+ @rtype: L{QueryState}
+ """
+
+ def set_label(label):
+ """
+ @param label:
+ @type label: string
+ """
+
Added: kiwi/trunk/kiwi/ui/search.py
==============================================================================
--- (empty file)
+++ kiwi/trunk/kiwi/ui/search.py Thu Apr 19 18:44:33 2007
@@ -0,0 +1,463 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2007 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin at async.com.br>
+#
+
+"""
+Search related widgets
+"""
+
+import datetime
+import gettext
+
+import gtk
+
+from kiwi.component import implements
+from kiwi.db.query import (NumberQueryState, StringQueryState,
+ DateQueryState, DateIntervalQueryState,
+ QueryExecuter)
+from kiwi.enums import SearchFilterPosition
+from kiwi.interfaces import ISearchFilter
+from kiwi.python import enum
+from kiwi.ui.dateentry import DateEntry
+from kiwi.ui.delegates import SlaveDelegate
+from kiwi.ui.objectlist import ObjectList
+from kiwi.ui.widgets.combo import ProxyComboBox
+
+_ = lambda m: gettext.dgettext('kiwi', m)
+
+class DateSearchOption(object):
+ """
+ Base class for Date search options
+ A date search option is an interval of dates
+ @cvar name: name of the search option
+ """
+ name = None
+
+ def get_interval(self):
+ """
+ @returns: start date, end date
+ @rtype: datetime.date tuple
+ """
+
+class Any(DateSearchOption):
+ name = _('Any')
+
+ def get_interval(self):
+ return None, None
+
+
+class Today(DateSearchOption):
+ name = _('Today')
+
+ def get_interval(self):
+ today = datetime.date.today()
+ return today, today
+
+
+class Yesterday(DateSearchOption):
+ name = _('Yesterday')
+
+ def get_interval(self):
+ yesterday = datetime.date.today() - datetime.timedelta(days=1)
+ return yesterday, yesterday
+
+
+class LastWeek(DateSearchOption):
+ name = _('Last week')
+
+ def get_interval(self):
+ today = datetime.date.today()
+ return (today - datetime.timedelta(days=7), today)
+
+
+class LastMonth(DateSearchOption):
+ name = _('Last month')
+
+ def get_interval(self):
+ today = datetime.date.today()
+ year = today.year
+ month = today.month - 1
+ if not month:
+ month = 12
+ year -= 1
+ # Try 31 first then remove one until date() does not complain.
+ day = today.day
+ while True:
+ try:
+ start_date = datetime.date(year, month, day)
+ break
+ except ValueError:
+ day -= 1
+ return start_date, datetime.date.today()
+
+
+class DateSearchFilter(object):
+ """
+ A filter which helps you to search by a date interval.
+ Can be customized through add_option.
+ """
+ implements(ISearchFilter)
+ class Type(enum):
+ (CUSTOM_DAY,
+ CUSTOM_INTERVAL) = range(100, 102)
+
+ def __init__(self, name):
+ self._options = {}
+ hbox = gtk.HBox()
+ hbox.set_border_width(6)
+ label = gtk.Label(name)
+ hbox.pack_start(label, False, False)
+ label.show()
+
+ self.mode = ProxyComboBox()
+ self.mode.connect('content-changed',
+ self._on_mode__content_changed)
+ hbox.pack_start(self.mode, False, False, 6)
+ self.mode.show()
+
+ self.from_label = gtk.Label(_("From:"))
+ hbox.pack_start(self.from_label, False, False)
+ self.from_label.show()
+
+ self.start_date = DateEntry()
+ self.start_date.connect('changed',
+ self._on_start_date__changed)
+ hbox.pack_start(self.start_date, False, False, 6)
+ self.start_date.show()
+
+ self.to_label = gtk.Label(_("To:"))
+ hbox.pack_start(self.to_label, False, False)
+ self.to_label.show()
+
+ self.end_date = DateEntry()
+ hbox.pack_start(self.end_date, False, False, 6)
+ self.end_date.show()
+
+ self.hbox = hbox
+
+ self.mode.prefill([
+ (_('Custom day'), DateSearchFilter.Type.CUSTOM_DAY),
+ (_('Custom interval'), DateSearchFilter.Type.CUSTOM_INTERVAL),
+ ])
+
+ for option in (Any, Today, Yesterday, LastWeek, LastMonth):
+ self.add_option(option)
+
+ self.mode.select_item_by_position(0)
+
+ def add_option(self, option_type):
+ """
+ Adds a date option
+ @param option_type: option to add
+ @type option_type: a L{DateSearchOption} subclass
+ """
+ option = option_type()
+ num = len(self.mode)-2
+ self.mode.insert_item(num, option.name, num)
+ self._options[num] = option
+
+ def get_widget(self):
+ return self.hbox
+
+ def get_state(self):
+ start = self.start_date.get_date()
+ end = self.end_date.get_date()
+ if start == end:
+ return DateQueryState(filter=self, date=start)
+ return DateIntervalQueryState(filter=self, start=start, end=end)
+
+ #
+ # Private
+ #
+
+ def _update_dates(self):
+ date_type = self.mode.get_selected_data()
+ if date_type not in [DateSearchFilter.Type.CUSTOM_DAY,
+ DateSearchFilter.Type.CUSTOM_INTERVAL]:
+ option = self._options.get(date_type)
+ start_date, end_date = option.get_interval()
+ self.start_date.set_date(start_date)
+ self.end_date.set_date(end_date)
+
+ def _update_sensitivity(self):
+ date_type = self.mode.get_selected_data()
+ enabled = date_type == DateSearchFilter.Type.CUSTOM_INTERVAL
+ self.to_label.set_sensitive(enabled)
+ self.end_date.set_sensitive(enabled)
+
+ enabled = (date_type == DateSearchFilter.Type.CUSTOM_INTERVAL or
+ date_type == DateSearchFilter.Type.CUSTOM_DAY)
+ self.from_label.set_sensitive(enabled)
+ self.start_date.set_sensitive(enabled)
+
+ #
+ # Callbacks
+ #
+
+ def _on_mode__content_changed(self, mode):
+ self._update_dates()
+ self._update_sensitivity()
+
+ def _on_start_date__changed(self, start_date):
+ date_type = self.mode.get_selected_data()
+ if date_type == DateSearchFilter.Type.CUSTOM_DAY:
+ self.end_date.set_date(start_date.get_date())
+
+
+class ComboSearchFilter(object):
+ implements(ISearchFilter)
+ def __init__(self, name, values):
+ hbox = gtk.HBox()
+ label = gtk.Label(name)
+ hbox.pack_start(label, False, False)
+ label.show()
+
+ self.combo = ProxyComboBox()
+ self.combo.prefill(values)
+ hbox.pack_start(self.combo, False, False, 6)
+ self.combo.show()
+
+ self.hbox = hbox
+
+ def get_widget(self):
+ return self.hbox
+
+ def get_state(self):
+ return NumberQueryState(filter=self,
+ value=self.combo.get_selected_data())
+
+
+class StringSearchFilter(object):
+ implements(ISearchFilter)
+ def __init__(self, name, chars=0):
+ hbox = gtk.HBox()
+ self.label = gtk.Label(name)
+ hbox.pack_start(self.label, False, False)
+ self.label.show()
+
+ self.entry = gtk.Entry()
+ if chars:
+ self.entry.set_width_chars(chars)
+ hbox.pack_start(self.entry, False, False, 6)
+ self.entry.show()
+
+ self.hbox = hbox
+
+ def get_widget(self):
+ return self.hbox
+
+ def get_state(self):
+ return StringQueryState(filter=self,
+ text=self.entry.get_text())
+
+ def set_label(self, label):
+ self.label.set_text(label)
+
+
+#
+# Other UI pieces
+#
+
+class SearchResults(ObjectList):
+ def __init__(self, columns):
+ ObjectList.__init__(self, columns)
+
+class SearchContainer(gtk.VBox):
+ """
+ A search container is a widget which consists of:
+ - search entry
+ - search button
+ - objectlist result
+
+ 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 L{SearchFilterPosition}
+ """
+ def __init__(self, columns=None, chars=25):
+ """
+ @param columns: a list of L{kiwi.ui.objectlist.Column}
+ @param chars: maximum number of chars used by the search entry
+ """
+ gtk.VBox.__init__(self)
+ self._columns = columns
+ self._search_filters = []
+ self._querty_executer = None
+
+ search_filter = StringSearchFilter(_('Search:'), chars=chars)
+ self._search_filters.append(search_filter)
+ self._primary_filter = search_filter
+
+ self._create_ui()
+
+
+ #
+ # Public API
+ #
+
+ def add_filter(self, search_filter, position=SearchFilterPosition.BOTTOM):
+ """
+ Adds a search filter
+ @param search_filter: the search filter
+ @param postition: a L{SearchFilterPosition} enum
+ """
+ if not ISearchFilter.providedBy(search_filter):
+ raise TypeError("search_filter must implement ISearchFilter")
+
+ widget = search_filter.get_widget()
+ assert not widget.parent
+ if position == SearchFilterPosition.TOP:
+ self.hbox.pack_start(widget, False, False)
+ self.hbox.reorder_child(widget, 0)
+ elif position == SearchFilterPosition.BOTTOM:
+ self.pack_start(widget, False, False)
+ widget.show()
+
+ self._search_filters.append(search_filter)
+
+ def set_query_executer(self, querty_executer):
+ """
+ Ties a QueryExecuter instance to the SearchContainer class
+ @param querty_executer: a querty executer
+ @type querty_executer: a L{QueryExecuter} subclass
+ """
+ if not isinstance(querty_executer, QueryExecuter):
+ raise TypeError("querty_executer must be a QueryExecuter instance")
+
+ self._query_executer = querty_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):
+ """
+ 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.results.clear()
+ self.results.extend(results)
+
+ #
+ # Callbacks
+ #
+
+ def _on_search_entry_activate(self, entry):
+ self.search()
+
+ def _on_search_button__clicked(self, button):
+ self.search()
+
+ #
+ # Private
+ #
+
+ def _create_ui(self):
+ hbox = gtk.HBox()
+ self.pack_start(hbox, False, False)
+ hbox.show()
+ self.hbox = hbox
+
+ widget = self._primary_filter.get_widget()
+ 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)
+
+ button = gtk.Button(stock=gtk.STOCK_FIND)
+ button.connect('clicked', self._on_search_button__clicked)
+ hbox.pack_start(button, False, False)
+ button.show()
+
+ self.results = SearchResults(self._columns)
+ self.pack_end(self.results)
+ self.results.show()
+
+class SearchSlaveDelegate(SlaveDelegate):
+ def __init__(self, columns):
+ self.search = SearchContainer(columns)
+ SlaveDelegate.__init__(self, toplevel=self.search)
+
+ #
+ # Public API
+ #
+
+ def add_filter(self, search_filter, position=SearchFilterPosition.BOTTOM):
+ """
+ Adds a search filter
+ @param search_filter: the search filter
+ @param postition: a L{SearchFilterPosition} enum
+ """
+ self.search.add_filter(search_filter, position)
+
+ def set_query_executer(self, querty_executer):
+ """
+ Ties a QueryExecuter instance to the SearchSlaveDelegate class
+ @param querty_executer: a querty executer
+ @type querty_executer: a L{QueryExecuter} subclass
+ """
+ if not isinstance(querty_executer, QueryExecuter):
+ raise TypeError("querty_executer must be a QueryExecuter instance")
+
+ self.search.set_query_executer(querty_executer)
+
+
+ def get_primary_filter(self):
+ """
+ Fetches the primary filter of the SearchSlaveDelegate
+ @returns: primary filter
+ """
+ return self.search.get_primary_filter()
+
+ def focus_search_entry(self):
+ """
+ Grabs the focus of the search entry
+ """
+ self.search.search_entry.grab_focus()
+
+ def search(self):
+ """
+ Trigger a search again with the currently selected inputs
+ """
+ self.search.search()
+
+ #
+ # Overridable
+ #
+
+ def get_columns(self):
+ """
+ This needs to be implemented in a subclass
+ @returns: columns
+ @rtype: list of L{kiwi.ui.objectlist.Column}
+ """
+ raise NotImplementedError
More information about the POS-commit
mailing list