root/trunk/camelot/view/filters.py @ 1184

Revision 1184, 13.7 KB (checked in by erikj, 6 months ago)

prepare complex filter widget

Line 
1#  ============================================================================
2#
3#  Copyright (C) 2007-2008 Conceptive Engineering bvba. All rights reserved.
4#  www.conceptive.be / project-camelot@conceptive.be
5#
6#  This file is part of the Camelot Library.
7#
8#  This file may be used under the terms of the GNU General Public
9#  License version 2.0 as published by the Free Software Foundation
10#  and appearing in the file LICENSE.GPL included in the packaging of
11#  this file.  Please review the following information to ensure GNU
12#  General Public Licensing requirements will be met:
13#  http://www.trolltech.com/products/qt/opensource.html
14#
15#  If you are unsure which license is appropriate for your use, please
16#  review the following information:
17#  http://www.trolltech.com/products/qt/licensing.html or contact
18#  project-camelot@conceptive.be.
19#
20#  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
21#  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
22#
23#  For use of this library in commercial applications, please contact
24#  project-camelot@conceptive.be
25#
26#  ============================================================================
27
28"""
29Python structures to represent filters.
30These structures can be transformed to QT forms.
31"""
32
33from camelot.view.model_thread import gui_function
34from camelot.core.utils import ugettext_lazy as _
35from camelot.core.utils import ugettext
36
37def structure_to_filter(structure):
38    """Convert a python data structure to a filter, using the following rules :
39   
40    if structure is an instance of Filter, return structure
41    else create a GroupBoxFilter from the structure
42    """
43    if isinstance(structure, Filter):
44        return structure
45    return GroupBoxFilter(structure)
46 
47class Filter(object):
48    """Base class for filters"""
49   
50    def __init__(self, attribute, value_to_string=lambda x:unicode(x)):
51        """
52        @param attribute: the attribute on which to filter, this attribute
53        may contain dots to indicate relationships that need to be followed,
54        eg.  'person.groups.name'
55        @param value_to_string: function that converts a value of the attribute to
56        a string that will be displayed in the filter
57        """
58        self.attribute = attribute
59        self._value_to_string = value_to_string
60   
61    @gui_function     
62    def render(self, parent, name, options):
63        """Render this filter as a qt object
64        @param parent: its parent widget
65        @param name: the name of the filter
66        @param options: the options that can be selected, where each option is a list
67        of tuples containting (option_name, query_decorator) 
68       
69        The name and the list of options can be fetched with get_name_and_options"""
70        raise NotImplementedError()
71       
72    def get_name_and_options(self, admin):
73        """return a tuple of the name of the filter and a list of options that can be selected.
74        Each option is a tuple of the name of the option, and a filter function to
75        decorate a query
76        @return:  (filter_name, [(option_name, query_decorator), ...)
77        """
78        from sqlalchemy.sql import select
79        from sqlalchemy import orm
80        from elixir import session
81        filter_names = []
82        joins = []
83        table = admin.entity.table
84        path = self.attribute.split('.')
85        for field_name in path:
86            attributes = admin.get_field_attributes(field_name)
87            filter_names.append(attributes['name'])
88            # @todo: if the filter is not on an attribute of the relation, but on the relation itselves
89            if 'target' in attributes:
90                admin = attributes['admin']
91                joins.append(field_name)
92                if attributes['direction'] == orm.interfaces.MANYTOONE:
93                    table = admin.entity.table.join(table)
94                else:
95                    table = admin.entity.table
96                 
97         
98        col = getattr(admin.entity, field_name)
99        query = select([col], distinct=True, order_by=col.asc()).select_from(table)
100         
101        def create_decorator(col, value, joins):
102         
103            def decorator(q):
104                if joins:
105                    q = q.join(joins, aliased=True)
106                return q.filter(col==value)
107             
108            return decorator
109     
110        options = [(self._value_to_string(value[0]), create_decorator(col, value[0], joins))
111                   for value in session.execute(query)]
112   
113        return (filter_names[0],[(_('all'), lambda q: q)] + options)
114       
115class GroupBoxFilter(Filter):
116    """Filter where the items are displayed in a QGroupBox"""
117   
118    @gui_function
119    def render(self, parent, name, options):
120     
121        from PyQt4 import QtCore, QtGui
122        from camelot.view.controls.filterlist import filter_changed_signal
123       
124        class FilterWidget(QtGui.QGroupBox):
125            """A box containing a filter that can be applied on a table view, this filter is
126            based on the distinct values in a certain column"""
127         
128            def __init__(self, name, choices, parent):
129                QtGui.QGroupBox.__init__(self, unicode(name), parent)
130                self.group = QtGui.QButtonGroup(self)
131                self.item = name
132                self.unique_values = []
133                self.choices = None
134                self.setChoices(choices)
135                 
136            def emit_filter_changed(self, state):
137                self.emit(filter_changed_signal)
138           
139            def setChoices(self, choices):
140                self.choices = choices
141                layout = QtGui.QVBoxLayout()
142                for i,name in enumerate([unicode(c[0]) for c in choices]):
143                    button = QtGui.QRadioButton(name, self)
144                    layout.addWidget(button)
145                    self.group.addButton(button, i)
146                    if i==0:
147                        button.setChecked(True)
148                    self.connect(button, QtCore.SIGNAL('toggled(bool)'), self.emit_filter_changed)
149                layout.addStretch()
150                self.setLayout(layout)
151           
152            def decorate_query(self, query):
153                checked = self.group.checkedId()
154                if checked>=0:
155                    return self.choices[checked][1](query)
156                return query
157               
158        return FilterWidget(name, options, parent)
159     
160class ComboBoxFilter(Filter):
161    """Filter where the items are displayed in a QComboBox"""
162   
163    @gui_function
164    def render(self, parent, name, options):
165     
166        from PyQt4 import QtCore, QtGui
167        from camelot.view.controls.filterlist import filter_changed_signal
168       
169        class FilterWidget(QtGui.QGroupBox):
170         
171            def __init__(self, name, choices, parent):
172                QtGui.QGroupBox.__init__(self, unicode(name), parent)
173                layout = QtGui.QVBoxLayout()
174                self.choices = choices
175                combobox = QtGui.QComboBox(self)
176                for i,(name,decorator) in enumerate(choices):
177                    combobox.insertItem(i, unicode(name), QtCore.QVariant(decorator))
178                layout.addWidget(combobox)
179                self.setLayout(layout)
180                self.current_index = 0
181                self.connect(combobox, QtCore.SIGNAL('currentIndexChanged(int)'), self.emit_filter_changed)
182                   
183            def emit_filter_changed(self, index):
184                self.current_index = index
185                self.emit(filter_changed_signal)
186               
187            def decorate_query(self, query):
188                if self.current_index>=0:
189                    return self.choices[self.current_index][1](query)
190                return query
191             
192        return FilterWidget(name, options, parent)
193   
194class EditorFilter(Filter):
195    """Filter that presents the user with an editor, allowing the user to enter
196    a value on which to filter, and at the same time to show 'All' or 'None'
197    """
198   
199    def __init__(self, field_name, verbose_name=None):
200        """:param field: the name of the field on which to filter"""
201        super(EditorFilter, self).__init__(field_name)
202        self._field_name = field_name
203        self._verbose_name = verbose_name
204       
205    def render(self, parent, name, options):
206       
207        from PyQt4 import QtCore, QtGui
208        from camelot.view.controls.filterlist import filter_changed_signal
209        from camelot.view.controls import editors
210               
211        class FilterWidget(QtGui.QGroupBox):
212           
213            def __init__(self, name, parent):
214                QtGui.QGroupBox.__init__(self, unicode(name), parent)
215                self._entity, self._field_name, self._field_attributes = options
216                self._field_attributes['editable'] = True
217                layout = QtGui.QVBoxLayout()
218                self._choices = [(0, ugettext('All')), (1, ugettext('None')), (2, ugettext('='))]
219                combobox = QtGui.QComboBox(self)
220                layout.addWidget(combobox)
221                for i,name in self._choices:
222                    combobox.insertItem(i, unicode(name))
223                self.connect(combobox, QtCore.SIGNAL('currentIndexChanged(int)'), self.combobox_changed)
224                delegate = self._field_attributes['delegate'](**self._field_attributes)
225                option = QtGui.QStyleOptionViewItem()
226                option.version = 5
227                self._editor = delegate.createEditor( self, option, None )
228                # explicitely set a value, otherways the current value remains ValueLoading
229                self._editor.set_value(None)
230                self.connect(self._editor, editors.editingFinished, self.editor_editing_finished)
231                layout.addWidget(self._editor)
232                self.setLayout(layout)
233                self._editor.setEnabled(False)
234                self._index = 0
235                self._value = None
236               
237            def combobox_changed(self, index):
238                self._index = index
239                if index==2:
240                    self._editor.setEnabled(True)
241                else:
242                    self._editor.setEnabled(False)
243                self.emit(filter_changed_signal)
244               
245            def editor_editing_finished(self):
246                self._value = self._editor.get_value()
247                self.emit(filter_changed_signal)
248           
249            def decorate_query(self, query):
250                if self._index==0:
251                    return query
252                if self._index==1:
253                    return query.filter(getattr(self._entity, self._field_name)==None)
254                return query.filter(getattr(self._entity, self._field_name)==self._value)
255               
256        return FilterWidget(name, parent)       
257   
258    def get_name_and_options(self, admin):
259        field_attributes = admin.get_field_attributes(self._field_name)
260        name = self._verbose_name or field_attributes['name']
261        return name, (admin.entity, self._field_name, field_attributes)
262       
263class ValidDateFilter(Filter):
264    """Filters entities that are valid a certain date.  This filter will present
265    a date to the user and filter the entities that have their from date before this
266    date and their end date after this date.  If no date is given, all entities will
267    be shown"""
268
269    def __init__(self, from_attribute='from_date', thru_attribute='thru_date', verbose_name=_('Valid at')):
270        """
271        :param from_attribute: the name of the attribute representing the from date
272        :param thru_attribute: the name of the attribute representing the thru date
273        :param verbose_name: the displayed name of the filter"""
274        self._from_attribute = from_attribute
275        self._thru_attribute = thru_attribute
276        self._verbose_name = verbose_name
277       
278    def render(self, parent, name, options):
279       
280        from datetime import date
281        from PyQt4 import QtGui, QtCore
282        from camelot.view.controls.filterlist import filter_changed_signal
283        from camelot.view.controls.editors import DateEditor, editingFinished
284       
285        class FilterWidget(QtGui.QGroupBox):
286         
287            def __init__(self, name, query_decorator, parent):
288                QtGui.QGroupBox.__init__(self, unicode(name), parent)
289                layout = QtGui.QVBoxLayout()
290                self.date_editor = DateEditor(parent=self, nullable=True)
291                self.date_editor.set_value(date.today())
292                self.query_decorator = query_decorator
293                layout.addWidget(self.date_editor)
294                self.setLayout(layout)
295                self.connect(self.date_editor, editingFinished, self.emit_filter_changed)
296                   
297            def emit_filter_changed(self):
298                self.emit(filter_changed_signal)
299               
300            def decorate_query(self, query):
301                return self.query_decorator(query, self.date_editor.get_value())
302             
303        return FilterWidget(name, options, parent)
304       
305    def get_name_and_options(self, admin):
306        from sqlalchemy.sql import and_
307       
308        def query_decorator(query, date):
309            e = admin.entity
310            if date:
311                return query.filter(and_(getattr(e, self._from_attribute)<=date,
312                                         getattr(e, self._thru_attribute)>=date))
313            return query
314       
315        return (self._verbose_name, query_decorator)
Note: See TracBrowser for help on using the browser.