| 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 | """
|
|---|
| 29 | Python structures to represent filters.
|
|---|
| 30 | These structures can be transformed to QT forms.
|
|---|
| 31 | """
|
|---|
| 32 |
|
|---|
| 33 | from camelot.view.model_thread import gui_function
|
|---|
| 34 | from camelot.core.utils import ugettext_lazy as _
|
|---|
| 35 | from camelot.core.utils import ugettext
|
|---|
| 36 |
|
|---|
| 37 | def 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 |
|
|---|
| 47 | class 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 |
|
|---|
| 115 | class 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 |
|
|---|
| 160 | class 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 |
|
|---|
| 194 | class 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 |
|
|---|
| 263 | class 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) |
|---|