Source code for camelot.view.filters

#  ============================================================================
#
#  Copyright (C) 2007-2011 Conceptive Engineering bvba. All rights reserved.
#  www.conceptive.be / project-camelot@conceptive.be
#
#  This file is part of the Camelot Library.
#
#  This file may be used under the terms of the GNU General Public
#  License version 2.0 as published by the Free Software Foundation
#  and appearing in the file license.txt included in the packaging of
#  this file.  Please review this information to ensure GNU
#  General Public Licensing requirements will be met.
#
#  If you are unsure which license is appropriate for your use, please
#  visit www.python-camelot.com or contact project-camelot@conceptive.be
#
#  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
#  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#
#  For use of this library in commercial applications, please contact
#  project-camelot@conceptive.be
#
#  ============================================================================

"""
Python structures to represent filters.
These structures can be transformed to QT forms.
"""

import datetime

from PyQt4 import QtCore, QtGui
from sqlalchemy import sql

from camelot.view.controls.editors import DateEditor
from camelot.view.model_thread import gui_function
from camelot.core.utils import ugettext_lazy as _

[docs]def structure_to_filter(structure): """Convert a python data structure to a filter, using the following rules : if structure is an instance of Filter, return structure else create a GroupBoxFilter from the structure """ if isinstance(structure, Filter): return structure return GroupBoxFilter(structure)
[docs]class Filter(object): """Base class for filters"""
[docs] class All(object): pass
def __init__(self, attribute, default=All): """ :param attribute: the attribute on which to filter, this attribute may contain dots to indicate relationships that need to be followed, eg. 'person.name' :param default: the default value to filter on when the view opens, defaults to showing all records. """ self.attribute = attribute self._default = default @gui_function
[docs] def render(self, parent, name, options): """Render this filter as a qt object @param parent: its parent widget @param name: the name of the filter @param options: the options that can be selected, where each option is a list of tuples containting (option_name, query_decorator) The name and the list of options can be fetched with get_name_and_options""" raise NotImplementedError()
[docs] def get_name_and_options(self, admin): """return a tuple of the name of the filter and a list of options that can be selected. Each option is a tuple of the name of the option, and a filter function to decorate a query @return: (filter_name, [(option_name, query_decorator), ...) """ from sqlalchemy.sql import select #from sqlalchemy.sql.expression import alias from sqlalchemy import orm from elixir import session filter_names = [] joins = [] # # in case of inheritance, use the local table to be able to join, # otherwise use the mapped table, to be able to filter on views # if admin.mapper!=admin.mapper.base_mapper: table = admin.mapper.local_table else: table = admin.mapper.mapped_table path = self.attribute.split('.') for field_name in path: attributes = admin.get_field_attributes(field_name) filter_names.append(attributes['name']) # @todo: if the filter is not on an attribute of the relation, but on the relation itselves if 'target' in attributes: admin = attributes['admin'] joins.append(field_name) if attributes['direction'] == orm.interfaces.MANYTOONE: table = admin.entity.table.join( table ) else: table = admin.entity.table col = getattr( admin.entity, field_name ) query = select([col], distinct=True, order_by=col.asc()).select_from(table) def create_decorator(col, attributes, value, joins): def decorator(q): if joins: q = q.join( *joins, aliased=True) if 'precision' in attributes: delta = pow( 10, -1*attributes['precision']) return q.filter( sql.and_(col < value+delta, col > value-delta) ) return q.filter(col==value) return decorator options = [] for value in session.execute(query): if 'to_string' in attributes: option_name = attributes['to_string'](value[0]) else: option_name = value[0] options.append((_(option_name), create_decorator(col, attributes, value[0], joins))) return (filter_names[0],[(_('All'), lambda q: q)] + options)
[docs]class FilterWidget( QtGui.QGroupBox ): """A box containing a filter that can be applied on a table view, this filter is based on the distinct values in a certain column""" filter_changed_signal = QtCore.pyqtSignal() def __init__(self, name, choices, parent): super( FilterWidget, self ).__init__( unicode( name ), parent ) layout = QtGui.QHBoxLayout() layout.setSpacing( 2 ) self.setLayout( layout ) self.group = QtGui.QButtonGroup(self) self.item = name self.unique_values = [] self.choices = None self.setChoices( choices ) @QtCore.pyqtSlot(bool)
[docs] def emit_filter_changed(self, state): self.filter_changed_signal.emit()
[docs] def setChoices(self, choices): self.choices = choices layout = self.layout() button_layout = QtGui.QVBoxLayout() for i, name in enumerate([unicode(c[0]) for c in choices]): button = QtGui.QRadioButton(name, self) button_layout.addWidget(button) self.group.addButton(button, i) if i==0: button.setChecked(True) button.toggled.connect( self.emit_filter_changed ) layout.addLayout( button_layout ) self.setLayout(layout)
[docs] def decorate_query(self, query): checked = self.group.checkedId() if checked>=0: return self.choices[checked][1](query) return query
[docs]class GroupBoxFilter(Filter): """Filter where the items are displayed in a QGroupBox""" @gui_function
[docs] def render(self, parent, name, options): return FilterWidget(name, options, parent)
[docs]class GroupBoxFilterWidget( QtGui.QGroupBox ): """Flter widget based on a QGroupBox""" filter_changed_signal = QtCore.pyqtSignal() def __init__(self, name, choices, parent): super( GroupBoxFilterWidget, self ).__init__( unicode( name ), parent ) layout = QtGui.QVBoxLayout() layout.setSpacing( 2 ) self.choices = choices combobox = QtGui.QComboBox(self) for i,(name,decorator) in enumerate(choices): combobox.insertItem(i, unicode(name), QtCore.QVariant(decorator)) layout.addWidget( combobox ) self.setLayout(layout) self.current_index = 0 combobox.currentIndexChanged.connect( self.emit_filter_changed ) @QtCore.pyqtSlot(int)
[docs] def emit_filter_changed(self, index): self.current_index = index self.filter_changed_signal.emit()
[docs] def decorate_query(self, query): if self.current_index>=0: return self.choices[self.current_index][1](query) return query
[docs]class ComboBoxFilter(Filter): """Filter where the items are displayed in a QComboBox""" @gui_function
[docs] def render(self, parent, name, options): return GroupBoxFilterWidget(name, options, parent)
[docs]class EditorFilter(Filter): """Filter that presents the user with an editor, allowing the user to enter a value on which to filter, and at the same time to show 'All' or 'None' """ def __init__(self, field_name, verbose_name=None): """:param field: the name of the field on which to filter""" super(EditorFilter, self).__init__(field_name) self._field_name = field_name self._verbose_name = verbose_name
[docs] def render(self, parent, name, options): from camelot.view.controls.filter_operator import FilterOperator entity, field_name, field_attributes = options return FilterOperator(entity, field_name, field_attributes, parent)
[docs] def get_name_and_options(self, admin): field_attributes = admin.get_field_attributes(self._field_name) name = self._verbose_name or field_attributes['name'] return name, (admin.entity, self._field_name, field_attributes)
[docs]class DateFilterWidget( QtGui.QGroupBox ): """Filter widget based on a DateEditor""" filter_changed_signal = QtCore.pyqtSignal() def __init__(self, name, query_decorator, default, parent): super( DateFilterWidget, self ).__init__( unicode( name ), parent ) layout = QtGui.QVBoxLayout() layout.setSpacing( 2 ) self.date_editor = DateEditor(parent=self, nullable=True) self.date_editor.set_value(default) self.query_decorator = query_decorator layout.addWidget( self.date_editor ) self.setLayout( layout ) self.date_editor.editingFinished.connect(self.emit_filter_changed) @QtCore.pyqtSlot()
[docs] def emit_filter_changed(self): self.filter_changed_signal.emit()
[docs] def decorate_query(self, query): return self.query_decorator(query, self.date_editor.get_value())
[docs]class ValidDateFilter(Filter): """Filters entities that are valid a certain date. This filter will present a date to the user and filter the entities that have their from date before this date and their end date after this date. If no date is given, all entities will be shown""" def __init__(self, from_attribute='from_date', thru_attribute='thru_date', verbose_name=_('Valid at'), default = datetime.date.today ): """ :param from_attribute: the name of the attribute representing the from date :param thru_attribute: the name of the attribute representing the thru date :param verbose_name: the displayed name of the filter :param default: a function returning a default date for the filter """ super(ValidDateFilter, self).__init__(None, default=default) self._from_attribute = from_attribute self._thru_attribute = thru_attribute self._verbose_name = verbose_name
[docs] def render(self, parent, name, options): query_decorator, default = options return DateFilterWidget(name, query_decorator, default, parent)
[docs] def get_name_and_options(self, admin): from sqlalchemy.sql import and_ def query_decorator(query, date): e = admin.entity if date: return query.filter(and_(getattr(e, self._from_attribute)<=date, getattr(e, self._thru_attribute)>=date)) return query return (self._verbose_name, (query_decorator, self._default()))

Comments
blog comments powered by Disqus