Skip to content

Commit

Permalink
Merge pull request #280 from alex/develop
Browse files Browse the repository at this point in the history
0.11.0 Release
  • Loading branch information
Carlton Gibson committed Aug 14, 2015
2 parents 6820300 + b8758f1 commit 70063a9
Show file tree
Hide file tree
Showing 19 changed files with 731 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.10.0
current_version = 0.11.0
commit = False
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
build/
dist/
docs/_build
.python-version
.tox
16 changes: 16 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
Version 0.11.0 (2015-08-14)
---------------------------

* FEATURE: Added default filter method lookup for MethodFilter #222

* FEATURE: Added support for yesterday in daterangefilter #234

* FEATURE: Created Filter for NumericRange. #236

* FEATURE: Added Date/time range filters #215

* FEATURE: Added option to raise with `strict` #255

* FEATURE: Added Form Field and Filter to parse ISO-8601 timestamps


Version 0.10.0 (2015-05-13)
---------------------

Expand Down
2 changes: 1 addition & 1 deletion django_filters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .filterset import FilterSet
from .filters import *

__version__ = '0.10.0'
__version__ = '0.11.0'


def parse_version(version):
Expand Down
72 changes: 67 additions & 5 deletions django_filters/fields.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,65 @@
from __future__ import absolute_import
from __future__ import unicode_literals

from datetime import datetime, time
from collections import namedtuple

from django import forms
from django.utils.dateparse import parse_datetime

# TODO: Remove this once Django 1.4 is EOL.
try:
from django.utils.encoding import force_str
except ImportError:
force_str = None

from .widgets import RangeWidget, LookupTypeWidget


class RangeField(forms.MultiValueField):
widget = RangeWidget

def __init__(self, *args, **kwargs):
fields = (
forms.DecimalField(),
forms.DecimalField(),
)
def __init__(self, fields=None, *args, **kwargs):
if fields is None:
fields = (
forms.DecimalField(),
forms.DecimalField())
super(RangeField, self).__init__(fields, *args, **kwargs)

def compress(self, data_list):
if data_list:
return slice(*data_list)
return None


class DateRangeField(RangeField):

def __init__(self, *args, **kwargs):
fields = (
forms.DateField(),
forms.DateField())
super(DateRangeField, self).__init__(fields, *args, **kwargs)

def compress(self, data_list):
if data_list:
start_date, stop_date = data_list
if start_date:
start_date = datetime.combine(start_date, time.min)
if stop_date:
stop_date = datetime.combine(stop_date, time.max)
return slice(start_date, stop_date)
return None


class TimeRangeField(RangeField):

def __init__(self, *args, **kwargs):
fields = (
forms.TimeField(),
forms.TimeField())
super(TimeRangeField, self).__init__(fields, *args, **kwargs)


Lookup = namedtuple('Lookup', ('value', 'lookup_type'))
class LookupTypeField(forms.MultiValueField):
def __init__(self, field, lookup_choices, *args, **kwargs):
Expand All @@ -41,3 +78,28 @@ def compress(self, data_list):
if len(data_list)==2:
return Lookup(value=data_list[0], lookup_type=data_list[1] or 'exact')
return Lookup(value=None, lookup_type='exact')


class IsoDateTimeField(forms.DateTimeField):
"""
Supports 'iso-8601' date format too which is out the scope of
the ``datetime.strptime`` standard library
# ISO 8601: ``http://www.w3.org/TR/NOTE-datetime``
Based on Gist example by David Medina https://gist.github.com/copitux/5773821
"""
ISO_8601 = 'iso-8601'
input_formats = [ISO_8601]

def strptime(self, value, format):
# TODO: Remove this once Django 1.4 is EOL.
if force_str is not None:
value = force_str(value)

if format == self.ISO_8601:
parsed = parse_datetime(value)
if parsed is None: # Continue with other formats if doesn't match
raise ValueError
return parsed
return super(IsoDateTimeField, self).strptime(value, format)
121 changes: 94 additions & 27 deletions django_filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _

from .fields import RangeField, LookupTypeField, Lookup
from .fields import (
RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)


__all__ = [
'Filter', 'CharFilter', 'BooleanFilter', 'ChoiceFilter',
'TypedChoiceFilter', 'MultipleChoiceFilter', 'DateFilter',
'DateTimeFilter', 'TimeFilter', 'ModelChoiceFilter',
'ModelMultipleChoiceFilter', 'NumberFilter', 'RangeFilter',
'DateRangeFilter', 'AllValuesFilter', 'MethodFilter'
'DateTimeFilter', 'IsoDateTimeFilter', 'TimeFilter', 'ModelChoiceFilter',
'ModelMultipleChoiceFilter', 'NumberFilter', 'NumericRangeFilter', 'RangeFilter',
'DateRangeFilter', 'DateFromToRangeFilter', 'TimeRangeFilter',
'AllValuesFilter', 'MethodFilter'
]


Expand All @@ -46,10 +48,18 @@ def __init__(self, name=None, label=None, widget=None, action=None,
self.creation_counter = Filter.creation_counter
Filter.creation_counter += 1

def get_method(self, qs):
"""Return filter method based on whether we're excluding
or simply filtering.
"""
return qs.exclude if self.exclude else qs.filter

@property
def field(self):
if not hasattr(self, '_field'):
help_text = _('This is an exclusion filter') if self.exclude else ''
help_text = self.extra.pop('help_text', None)
if help_text is None:
help_text = _('This is an exclusion filter') if self.exclude else _('Filter')
if (self.lookup_type is None or
isinstance(self.lookup_type, (list, tuple))):
if self.lookup_type is None:
Expand All @@ -74,8 +84,7 @@ def filter(self, qs, value):
lookup = self.lookup_type
if value in ([], (), {}, None, ''):
return qs
method = qs.exclude if self.exclude else qs.filter
qs = method(**{'%s__%s' % (self.name, lookup): value})
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
if self.distinct:
qs = qs.distinct()
return qs
Expand All @@ -90,7 +99,7 @@ class BooleanFilter(Filter):

def filter(self, qs, value):
if value is not None:
return qs.filter(**{self.name: value})
return self.get_method(qs)(**{self.name: value})
return qs


Expand All @@ -109,19 +118,28 @@ class MultipleChoiceFilter(Filter):
Advanced Use
------------
Depending on your application logic, when all or no choices are selected, filtering may be a noop. In this case you may wish to avoid the filtering overhead, particularly of the `distinct` call.
Depending on your application logic, when all or no choices are selected,
filtering may be a noop. In this case you may wish to avoid the filtering
overhead, particularly if using a `distinct` call.
Set `always_filter` to False after instantiation to enable the default `is_noop` test.
Set `always_filter` to False after instantiation to enable the default
`is_noop` test.
Override `is_noop` if you require a different test for your application.
`distinct` defaults to True on this class to preserve backward compatibility.
"""
field_class = forms.MultipleChoiceField

always_filter = True

def __init__(self, *args, **kwargs):
distinct = kwargs.get('distinct', True)
kwargs['distinct'] = distinct

conjoined = kwargs.pop('conjoined', False)
self.conjoined = conjoined

super(MultipleChoiceFilter, self).__init__(*args, **kwargs)

def is_noop(self, qs, value):
Expand All @@ -147,15 +165,17 @@ def filter(self, qs, value):
if not value:
return qs

if self.conjoined:
for v in value:
qs = qs.filter(**{self.name: v})
return qs

q = Q()
for v in value:
q |= Q(**{self.name: v})
return qs.filter(q).distinct()
for v in set(value):
if self.conjoined:
qs = self.get_method(qs)(**{self.name: v})
else:
q |= Q(**{self.name: v})

if self.distinct:
return self.get_method(qs)(q).distinct()

return self.get_method(qs)(q)


class DateFilter(Filter):
Expand All @@ -165,6 +185,17 @@ class DateFilter(Filter):
class DateTimeFilter(Filter):
field_class = forms.DateTimeField

class IsoDateTimeFilter(DateTimeFilter):
"""
Uses IsoDateTimeField to support filtering on ISO 8601 formated datetimes.
For context see:
* https://code.djangoproject.com/ticket/23448
* https://github.com/tomchristie/django-rest-framework/issues/1338
* https://github.com/alex/django-filter/pull/264
"""
field_class = IsoDateTimeField

class TimeFilter(Filter):
field_class = forms.TimeField
Expand All @@ -182,19 +213,36 @@ class NumberFilter(Filter):
field_class = forms.DecimalField


class NumericRangeFilter(Filter):
field_class = RangeField

def filter(self, qs, value):
if value:
if value.start is not None and value.stop is not None:
lookup = '%s__%s' % (self.name, self.lookup_type)
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start is not None:
qs = self.get_method(qs)(**{'%s__startswith' % self.name: value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__endswith' % self.name: value.stop})
return qs


class RangeFilter(Filter):
field_class = RangeField

def filter(self, qs, value):
if value:
if value.start and value.stop:
if value.start is not None and value.stop is not None:
lookup = '%s__range' % self.name
return qs.filter(**{lookup: (value.start, value.stop)})
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start:
qs = qs.filter(**{'%s__gte'%self.name:value.start})
if value.stop:
qs = qs.filter(**{'%s__lte'%self.name:value.stop})

if value.start is not None:
qs = self.get_method(qs)(**{'%s__gte'%self.name:value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__lte'%self.name:value.stop})
return qs


Expand All @@ -220,6 +268,11 @@ class DateRangeFilter(ChoiceFilter):
4: (_('This year'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
})),
5: (_('Yesterday'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month,
'%s__day' % name: (now() - timedelta(days=1)).day,
})),
}

def __init__(self, *args, **kwargs):
Expand All @@ -235,6 +288,14 @@ def filter(self, qs, value):
return self.options[value][1](qs, self.name)


class DateFromToRangeFilter(RangeFilter):
field_class = DateRangeField


class TimeRangeFilter(RangeFilter):
field_class = TimeRangeField


class AllValuesFilter(ChoiceFilter):
@property
def field(self):
Expand Down Expand Up @@ -268,12 +329,18 @@ def __init__(self, *args, **kwargs):

def filter(self, qs, value):
"""
This filter method will act as a proxy for the actual method we want to call.
It will try to find the method on the parent filterset, if not it defaults
to just returning the queryset
This filter method will act as a proxy for the actual method we want to
call.
It will try to find the method on the parent filterset,
if not it attempts to search for the method `field_{{attribute_name}}`.
Otherwise it defaults to just returning the queryset.
"""
parent = getattr(self, 'parent', None)
parent_filter_method = getattr(parent, self.parent_action, None)
if not parent_filter_method:
func_str = 'filter_{0}'.format(self.name)
parent_filter_method = getattr(parent, func_str, None)
if parent_filter_method is not None:
return parent_filter_method(qs, value)
return qs

0 comments on commit 70063a9

Please sign in to comment.