commit
d2bda5a3b9
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 0.11.0
|
||||
current_version = 0.12.0
|
||||
commit = False
|
||||
tag = False
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
||||
|
|
31
.travis.yml
31
.travis.yml
|
@ -1,21 +1,18 @@
|
|||
language: python
|
||||
|
||||
sudo: false
|
||||
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
#- "pypy"
|
||||
#- "pypy3"
|
||||
- "3.5"
|
||||
|
||||
env:
|
||||
- DJANGO='https://github.com/django/django/archive/master.tar.gz'
|
||||
- DJANGO='django>=1.9.0,<1.10.0'
|
||||
- DJANGO='django>=1.8.0,<1.9.0'
|
||||
- DJANGO='django>=1.7.0,<1.8.0'
|
||||
- DJANGO='django>=1.6.0,<1.7.0'
|
||||
- DJANGO='django>=1.5.0,<1.6.0'
|
||||
- DJANGO='django>=1.4.0,<1.5.0'
|
||||
|
||||
install:
|
||||
- travis_retry pip install $DJANGO
|
||||
|
@ -31,21 +28,13 @@ notifications:
|
|||
matrix:
|
||||
exclude:
|
||||
- python: "3.2"
|
||||
env: DJANGO='django>=1.4.0,<1.5.0'
|
||||
- python: "3.3"
|
||||
env: DJANGO='django>=1.4.0,<1.5.0'
|
||||
- python: "3.4"
|
||||
env: DJANGO='django>=1.4.0,<1.5.0'
|
||||
#- python: "pypy"
|
||||
# env: DJANGO='django>=1.4.0,<1.5.0'
|
||||
#- python: "pypy3"
|
||||
# env: DJANGO='django>=1.4.0,<1.5.0'
|
||||
- python: "2.6"
|
||||
env: DJANGO='django>=1.7.0,<1.8.0'
|
||||
- python: "2.6"
|
||||
env: DJANGO='django>=1.8.0,<1.9.0'
|
||||
- python: "2.6"
|
||||
env: DJANGO='https://github.com/django/django/archive/master.tar.gz'
|
||||
- python: "3.2"
|
||||
env: DJANGO='django>=1.9.0,<1.10.0'
|
||||
- python: "3.3"
|
||||
env: DJANGO='https://github.com/django/django/archive/master.tar.gz'
|
||||
- python: "3.3"
|
||||
env: DJANGO='django>=1.9.0,<1.10.0'
|
||||
allow_failures:
|
||||
- env: DJANGO='https://github.com/django/django/archive/master.tar.gz'
|
||||
fast_finish: true
|
||||
|
|
26
CHANGES.rst
26
CHANGES.rst
|
@ -1,3 +1,29 @@
|
|||
Version 0.12.0 (2016-01-07)
|
||||
---------------------------
|
||||
|
||||
* FEATURE: Add support for custom ORM lookup types #221
|
||||
|
||||
* FEATURE: Add JavaScript friendly BooleanWidget #270
|
||||
|
||||
* FIXED: (More) Compatability with Django 1.8 and Django 1.9+
|
||||
|
||||
* BREAKING CHANGE: custom filter names are now also be used for ordering #230
|
||||
|
||||
If you use ordering on a field you defined as custom filter with custom
|
||||
name, you should now use the filter name as ordering key as well.
|
||||
|
||||
Eg. For a filter like :
|
||||
|
||||
class F(FilterSet):
|
||||
account = CharFilter(name='username')
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['account', 'status']
|
||||
order_by = True
|
||||
|
||||
Before, ordering was like `?o=username`. Since 0.12.0 it's `o=account`.
|
||||
|
||||
|
||||
Version 0.11.0 (2015-08-14)
|
||||
---------------------------
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ Full documentation on `read the docs`_.
|
|||
Requirements
|
||||
------------
|
||||
|
||||
* Python 2.6+
|
||||
* Django 1.4.5+
|
||||
* Python 2.7, 3.2, 3.3, 3.4, 3.5
|
||||
* Django 1.8, 1.9
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
@ -50,7 +50,7 @@ And then in your view you could do::
|
|||
|
||||
Django-filters additionally supports specifying FilterSet fields using a
|
||||
dictionary to specify filters with lookup types::
|
||||
|
||||
|
||||
import django_filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import absolute_import
|
|||
from .filterset import FilterSet
|
||||
from .filters import *
|
||||
|
||||
__version__ = '0.11.0'
|
||||
__version__ = '0.12.0'
|
||||
|
||||
|
||||
def parse_version(version):
|
||||
|
|
|
@ -5,13 +5,11 @@ from datetime import datetime, time
|
|||
from collections import namedtuple
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils import timezone
|
||||
|
||||
# TODO: Remove this once Django 1.4 is EOL.
|
||||
try:
|
||||
from django.utils.encoding import force_str
|
||||
except ImportError:
|
||||
force_str = None
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from .widgets import RangeWidget, LookupTypeWidget
|
||||
|
||||
|
@ -61,6 +59,8 @@ class TimeRangeField(RangeField):
|
|||
|
||||
|
||||
Lookup = namedtuple('Lookup', ('value', 'lookup_type'))
|
||||
|
||||
|
||||
class LookupTypeField(forms.MultiValueField):
|
||||
def __init__(self, field, lookup_choices, *args, **kwargs):
|
||||
fields = (
|
||||
|
@ -75,7 +75,7 @@ class LookupTypeField(forms.MultiValueField):
|
|||
super(LookupTypeField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
def compress(self, data_list):
|
||||
if len(data_list)==2:
|
||||
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')
|
||||
|
||||
|
@ -91,15 +91,21 @@ class IsoDateTimeField(forms.DateTimeField):
|
|||
"""
|
||||
ISO_8601 = 'iso-8601'
|
||||
input_formats = [ISO_8601]
|
||||
default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
|
||||
|
||||
def strptime(self, value, format):
|
||||
# TODO: Remove this once Django 1.4 is EOL.
|
||||
if force_str is not None:
|
||||
value = force_str(value)
|
||||
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
|
||||
|
||||
# Handle timezone awareness. Copied from:
|
||||
# https://github.com/tomchristie/django-rest-framework/blob/3.2.0/rest_framework/fields.py#L965-L969
|
||||
if settings.USE_TZ and not timezone.is_aware(parsed):
|
||||
return timezone.make_aware(parsed, self.default_timezone)
|
||||
elif not settings.USE_TZ and timezone.is_aware(parsed):
|
||||
return timezone.make_naive(parsed, timezone.UTC())
|
||||
return parsed
|
||||
return super(IsoDateTimeField, self).strptime(value, format)
|
||||
|
|
|
@ -33,7 +33,7 @@ class Filter(object):
|
|||
field_class = forms.Field
|
||||
|
||||
def __init__(self, name=None, label=None, widget=None, action=None,
|
||||
lookup_type='exact', required=False, distinct=False, exclude=False, **kwargs):
|
||||
lookup_type='exact', required=False, distinct=False, exclude=False, **kwargs):
|
||||
self.name = name
|
||||
self.label = label
|
||||
if action:
|
||||
|
@ -62,18 +62,27 @@ class Filter(object):
|
|||
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:
|
||||
lookup = [(x, x) for x in LOOKUP_TYPES]
|
||||
else:
|
||||
lookup = [
|
||||
(x, x) for x in LOOKUP_TYPES if x in self.lookup_type]
|
||||
|
||||
lookup = []
|
||||
|
||||
for x in LOOKUP_TYPES:
|
||||
if isinstance(x, (list, tuple)) and len(x) == 2:
|
||||
choice = (x[0], x[1])
|
||||
else:
|
||||
choice = (x, x)
|
||||
|
||||
if self.lookup_type is None:
|
||||
lookup.append(choice)
|
||||
elif x in self.lookup_type:
|
||||
lookup.append(choice)
|
||||
|
||||
self._field = LookupTypeField(self.field_class(
|
||||
required=self.required, widget=self.widget, **self.extra),
|
||||
lookup, required=self.required, label=self.label, help_text=help_text)
|
||||
else:
|
||||
self._field = self.field_class(required=self.required,
|
||||
label=self.label, widget=self.widget,
|
||||
help_text=help_text, **self.extra)
|
||||
label=self.label, widget=self.widget,
|
||||
help_text=help_text, **self.extra)
|
||||
return self._field
|
||||
|
||||
def filter(self, qs, value):
|
||||
|
@ -84,9 +93,9 @@ class Filter(object):
|
|||
lookup = self.lookup_type
|
||||
if value in ([], (), {}, None, ''):
|
||||
return qs
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
|
||||
return qs
|
||||
|
||||
|
||||
|
@ -97,11 +106,6 @@ class CharFilter(Filter):
|
|||
class BooleanFilter(Filter):
|
||||
field_class = forms.NullBooleanField
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value is not None:
|
||||
return self.get_method(qs)(**{self.name: value})
|
||||
return qs
|
||||
|
||||
|
||||
class ChoiceFilter(Filter):
|
||||
field_class = forms.ChoiceField
|
||||
|
@ -111,6 +115,10 @@ class TypedChoiceFilter(Filter):
|
|||
field_class = forms.TypedChoiceField
|
||||
|
||||
|
||||
class UUIDFilter(Filter):
|
||||
field_class = forms.UUIDField
|
||||
|
||||
|
||||
class MultipleChoiceFilter(Filter):
|
||||
"""
|
||||
This filter preforms OR(by default) or AND(using conjoined=True) query
|
||||
|
@ -156,7 +164,7 @@ class MultipleChoiceFilter(Filter):
|
|||
return False
|
||||
|
||||
def filter(self, qs, value):
|
||||
value = value or () # Make sure we have an iterable
|
||||
value = value or () # Make sure we have an iterable
|
||||
|
||||
if self.is_noop(qs, value):
|
||||
return qs
|
||||
|
@ -185,6 +193,7 @@ class DateFilter(Filter):
|
|||
class DateTimeFilter(Filter):
|
||||
field_class = forms.DateTimeField
|
||||
|
||||
|
||||
class IsoDateTimeFilter(DateTimeFilter):
|
||||
"""
|
||||
Uses IsoDateTimeField to support filtering on ISO 8601 formated datetimes.
|
||||
|
@ -197,6 +206,7 @@ class IsoDateTimeFilter(DateTimeFilter):
|
|||
"""
|
||||
field_class = IsoDateTimeField
|
||||
|
||||
|
||||
class TimeFilter(Filter):
|
||||
field_class = forms.TimeField
|
||||
|
||||
|
@ -234,19 +244,19 @@ class RangeFilter(Filter):
|
|||
|
||||
def filter(self, qs, value):
|
||||
if value:
|
||||
if value.start is not None and value.stop is not None:
|
||||
lookup = '%s__range' % self.name
|
||||
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
|
||||
else:
|
||||
|
||||
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})
|
||||
if value.start is not None and value.stop is not None:
|
||||
lookup = '%s__range' % self.name
|
||||
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
_truncate = lambda dt: dt.replace(hour=0, minute=0, second=0)
|
||||
def _truncate(dt):
|
||||
return dt.replace(hour=0, minute=0, second=0)
|
||||
|
||||
|
||||
class DateRangeFilter(ChoiceFilter):
|
||||
|
@ -285,7 +295,10 @@ class DateRangeFilter(ChoiceFilter):
|
|||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
value = ''
|
||||
return self.options[value][1](qs, self.name)
|
||||
qs = self.options[value][1](qs, self.name)
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
||||
|
||||
class DateFromToRangeFilter(RangeFilter):
|
||||
|
|
|
@ -1,54 +1,29 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import types
|
||||
import copy
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.forms.forms import NON_FIELD_ERRORS
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
from django.db import models
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.utils import six
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext as _
|
||||
from sys import version_info
|
||||
|
||||
try:
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
except ImportError: # pragma: nocover
|
||||
# Django < 1.5 fallback
|
||||
from django.db.models.sql.constants import LOOKUP_SEP # noqa
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError: # pragma: nocover
|
||||
# Django < 1.5 fallback
|
||||
from django.utils.datastructures import SortedDict as OrderedDict # noqa
|
||||
|
||||
try:
|
||||
from django.db.models.related import RelatedObject as ForeignObjectRel
|
||||
except ImportError: # pragma: nocover
|
||||
# Django >= 1.8 replaces RelatedObject with ForeignObjectRel
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
|
||||
|
||||
from .filters import (Filter, CharFilter, BooleanFilter,
|
||||
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter, NumberFilter)
|
||||
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter)
|
||||
|
||||
|
||||
ORDER_BY_FIELD = 'o'
|
||||
|
||||
|
||||
# There is a bug with deepcopy in 2.6, patch if we are running python < 2.7
|
||||
# http://bugs.python.org/issue1515
|
||||
if version_info < (2, 7, 0):
|
||||
def _deepcopy_method(x, memo):
|
||||
return type(x)(x.im_func, copy.deepcopy(x.im_self, memo), x.im_class)
|
||||
copy._deepcopy_dispatch[types.MethodType] = _deepcopy_method
|
||||
|
||||
|
||||
class STRICTNESS(object):
|
||||
"""
|
||||
Values of False & True chosen for backward compatability reasons.
|
||||
|
@ -86,21 +61,15 @@ def get_model_field(model, f):
|
|||
opts = model._meta
|
||||
for name in parts[:-1]:
|
||||
try:
|
||||
rel = opts.get_field_by_name(name)[0]
|
||||
rel = opts.get_field(name)
|
||||
except FieldDoesNotExist:
|
||||
return None
|
||||
if isinstance(rel, ForeignObjectRel):
|
||||
if hasattr(rel, "related_model"):
|
||||
# django >= 1.8 (ForeignObjectRel)
|
||||
opts = rel.related_model._meta
|
||||
else:
|
||||
# django < 1.8 (RelatedObject)
|
||||
opts = rel.opts
|
||||
opts = rel.related_model._meta
|
||||
else:
|
||||
model = rel.rel.to
|
||||
opts = model._meta
|
||||
opts = rel.rel.to._meta
|
||||
try:
|
||||
rel, model, direct, m2m = opts.get_field_by_name(parts[-1])
|
||||
rel = opts.get_field(parts[-1])
|
||||
except FieldDoesNotExist:
|
||||
return None
|
||||
return rel
|
||||
|
@ -112,7 +81,7 @@ def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
|
|||
opts = model._meta
|
||||
if fields is None:
|
||||
fields = [f.name for f in sorted(opts.fields + opts.many_to_many)
|
||||
if not isinstance(f, models.AutoField)]
|
||||
if not isinstance(f, models.AutoField)]
|
||||
# Loop through the list of fields.
|
||||
for f in fields:
|
||||
# Skip the field if excluded.
|
||||
|
@ -149,18 +118,18 @@ def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
|
|||
|
||||
def get_full_clean_override(together):
|
||||
def full_clean(form):
|
||||
|
||||
|
||||
def add_error(message):
|
||||
try:
|
||||
form.add_error(None, message)
|
||||
except AttributeError:
|
||||
form._errors[NON_FIELD_ERRORS] = message
|
||||
|
||||
|
||||
def all_valid(fieldset):
|
||||
cleaned_data = form.cleaned_data
|
||||
count = len([i for i in fieldset if cleaned_data.get(i)])
|
||||
return 0 < count < len(fieldset)
|
||||
|
||||
|
||||
super(form.__class__, form).full_clean()
|
||||
message = 'Following fields must be together: %s'
|
||||
if isinstance(together[0], (list, tuple)):
|
||||
|
@ -181,7 +150,7 @@ class FilterSetOptions(object):
|
|||
self.order_by = getattr(options, 'order_by', False)
|
||||
|
||||
self.form = getattr(options, 'form', forms.Form)
|
||||
|
||||
|
||||
self.together = getattr(options, 'together', None)
|
||||
|
||||
|
||||
|
@ -211,7 +180,7 @@ class FilterSetMetaclass(type):
|
|||
|
||||
if None in filters.values():
|
||||
raise TypeError("Meta.fields contains a field that isn't defined "
|
||||
"on this FilterSet")
|
||||
"on this FilterSet")
|
||||
|
||||
new_class.declared_filters = declared_filters
|
||||
new_class.base_filters = filters
|
||||
|
@ -296,12 +265,15 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
|
|||
models.URLField: {
|
||||
'filter_class': CharFilter,
|
||||
},
|
||||
models.IPAddressField: {
|
||||
models.GenericIPAddressField: {
|
||||
'filter_class': CharFilter,
|
||||
},
|
||||
models.CommaSeparatedIntegerField: {
|
||||
'filter_class': CharFilter,
|
||||
},
|
||||
models.UUIDField: {
|
||||
'filter_class': UUIDFilter,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -335,7 +307,7 @@ class BaseFilterSet(object):
|
|||
yield obj
|
||||
|
||||
def __len__(self):
|
||||
return len(self.qs)
|
||||
return self.qs.count()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.qs[key]
|
||||
|
@ -420,16 +392,21 @@ class BaseFilterSet(object):
|
|||
# e.g. (('field', 'Display name'), ...)
|
||||
choices = [(f[0], f[1]) for f in self._meta.order_by]
|
||||
else:
|
||||
choices = [(f, _('%s (descending)' % capfirst(f[1:])) if f[0] == '-' else capfirst(f))
|
||||
for f in self._meta.order_by]
|
||||
choices = []
|
||||
for f in self._meta.order_by:
|
||||
if f[0] == '-':
|
||||
label = _('%s (descending)' % capfirst(f[1:]))
|
||||
else:
|
||||
label = capfirst(f)
|
||||
choices.append((f, label))
|
||||
else:
|
||||
# add asc and desc field names
|
||||
# use the filter's label if provided
|
||||
choices = []
|
||||
for f, fltr in self.filters.items():
|
||||
choices.extend([
|
||||
(fltr.name or f, fltr.label or capfirst(f)),
|
||||
("-%s" % (fltr.name or f), _('%s (descending)' % (fltr.label or capfirst(f))))
|
||||
(f, fltr.label or capfirst(f)),
|
||||
("-%s" % (f), _('%s (descending)' % (fltr.label or capfirst(f))))
|
||||
])
|
||||
return forms.ChoiceField(label=_("Ordering"), required=False,
|
||||
choices=choices)
|
||||
|
@ -441,6 +418,15 @@ class BaseFilterSet(object):
|
|||
return self._ordering_field
|
||||
|
||||
def get_order_by(self, order_choice):
|
||||
re_ordering_field = re.compile(r'(?P<inverse>\-?)(?P<field>.*)')
|
||||
m = re.match(re_ordering_field, order_choice)
|
||||
inverted = m.group('inverse')
|
||||
filter_api_name = m.group('field')
|
||||
|
||||
_filter = self.filters.get(filter_api_name, None)
|
||||
|
||||
if _filter and filter_api_name != _filter.name:
|
||||
return [inverted + _filter.name]
|
||||
return [order_choice]
|
||||
|
||||
@classmethod
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,47 @@
|
|||
# Django Filter translation.
|
||||
# Copyright (C) 2013
|
||||
# This file is distributed under the same license as the django_filter package.
|
||||
# Gonzalo Bustos, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-07-05 19:24+0200\n"
|
||||
"PO-Revision-Date: 2015-10-11 20:53-0300\n"
|
||||
"Last-Translator: Gonzalo Bustos\n"
|
||||
"Language-Team: Spanish (Argentina)\n"
|
||||
"Language: es_AR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.6.10\n"
|
||||
|
||||
#: filters.py:51
|
||||
msgid "This is an exclusion filter"
|
||||
msgstr "Este es un filtro de exclusión"
|
||||
|
||||
#: filters.py:158
|
||||
msgid "Any date"
|
||||
msgstr "Cualquier fecha"
|
||||
|
||||
#: filters.py:159
|
||||
msgid "Today"
|
||||
msgstr "Hoy"
|
||||
|
||||
#: filters.py:164
|
||||
msgid "Past 7 days"
|
||||
msgstr "Últimos 7 días"
|
||||
|
||||
#: filters.py:168
|
||||
msgid "This month"
|
||||
msgstr "Este mes"
|
||||
|
||||
#: filters.py:172
|
||||
msgid "This year"
|
||||
msgstr "Este año"
|
||||
|
||||
#: widgets.py:63
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
Binary file not shown.
|
@ -0,0 +1,65 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django_filters 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-07-25 01:24+0200\n"
|
||||
"PO-Revision-Date: 2015-07-25 01:27+0100\n"
|
||||
"Last-Translator: Adam Dobrawy <naczelnik@jawnosc.tk>\n"
|
||||
"Language-Team: Adam Dobrawy <naczelnik@jawnosc.tk>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2);\n"
|
||||
"X-Generator: Poedit 1.5.4\n"
|
||||
"Language: pl_PL\n"
|
||||
|
||||
#: filters.py:56
|
||||
msgid "This is an exclusion filter"
|
||||
msgstr "Jest to filtr wykluczający"
|
||||
|
||||
#: filters.py:56
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
|
||||
#: filters.py:226
|
||||
msgid "Any date"
|
||||
msgstr "Dowolna data"
|
||||
|
||||
#: filters.py:227
|
||||
msgid "Today"
|
||||
msgstr "Dziś"
|
||||
|
||||
#: filters.py:232
|
||||
msgid "Past 7 days"
|
||||
msgstr "Ostatnie 7 dni"
|
||||
|
||||
#: filters.py:236
|
||||
msgid "This month"
|
||||
msgstr "Ten miesiąc"
|
||||
|
||||
#: filters.py:240
|
||||
msgid "This year"
|
||||
msgstr "Ten rok"
|
||||
|
||||
#: filters.py:243
|
||||
msgid "Yesterday"
|
||||
msgstr "Wczoraj"
|
||||
|
||||
#: filterset.py:423 filterset.py:432
|
||||
#, python-format
|
||||
msgid "%s (descending)"
|
||||
msgstr "%s (malejąco)"
|
||||
|
||||
#: filterset.py:434
|
||||
msgid "Ordering"
|
||||
msgstr "Sortowanie"
|
||||
|
||||
#: widgets.py:63
|
||||
msgid "All"
|
||||
msgstr "Wszystko"
|
|
@ -10,10 +10,7 @@ except:
|
|||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms.widgets import flatatt
|
||||
try:
|
||||
from django.utils.encoding import force_text
|
||||
except: # pragma: nocover
|
||||
from django.utils.encoding import force_unicode as force_text # noqa
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
@ -80,7 +77,7 @@ class LinkWidget(forms.Widget):
|
|||
|
||||
class RangeWidget(forms.MultiWidget):
|
||||
def __init__(self, attrs=None):
|
||||
widgets = (forms.TextInput(attrs=attrs), forms.TextInput(attrs=attrs))
|
||||
widgets = (forms.TextInput, forms.TextInput)
|
||||
super(RangeWidget, self).__init__(widgets, attrs)
|
||||
|
||||
def decompress(self, value):
|
||||
|
@ -97,3 +94,23 @@ class LookupTypeWidget(forms.MultiWidget):
|
|||
if value is None:
|
||||
return [None, None]
|
||||
return value
|
||||
|
||||
|
||||
class BooleanWidget(forms.Widget):
|
||||
"""Convert true/false values into the internal Python True/False.
|
||||
This can be used for AJAX queries that pass true/false from JavaScript's
|
||||
internal types through.
|
||||
"""
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""
|
||||
"""
|
||||
value = super(BooleanWidget, self).value_from_datadict(
|
||||
data, files, name)
|
||||
|
||||
if value is not None:
|
||||
if value.lower() == 'true':
|
||||
value = True
|
||||
elif value.lower() == 'false':
|
||||
value = False
|
||||
|
||||
return value
|
||||
|
|
|
@ -48,9 +48,9 @@ copyright = u'2013, Alex Gaynor and others.'
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.11.0'
|
||||
version = '0.12.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.11.0'
|
||||
release = '0.12.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
|
@ -12,6 +12,11 @@ Filters
|
|||
This filter does simple character matches, used with ``CharField`` and
|
||||
``TextField`` by default.
|
||||
|
||||
``UUIDFilter``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This filter matches UUID values, used with ``models.UUIDField`` by default.
|
||||
|
||||
``BooleanFilter``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -132,6 +137,16 @@ Similar to a ``ChoiceFilter`` except it works with related models, used for
|
|||
If automatically instantiated ``ModelChoiceFilter`` will use the default ``QuerySet`` for the
|
||||
related field. If manually instantiated you **must** provide the ``queryset`` kwarg.
|
||||
|
||||
Example. ::
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for books by author"""
|
||||
author = ModelChoiceFilter(queryset=Author.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['author']
|
||||
|
||||
``ModelMultipleChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -217,11 +232,28 @@ of the admin.
|
|||
|
||||
This is a ``Filter`` that will allow you to run a method that exists on the filter set that
|
||||
this filter is a property of. Set the `action` to a string that will map to a method on the
|
||||
filter set class.
|
||||
filter set class. `action` will default to ``filter_{value-of-name-attribute}``
|
||||
|
||||
Example. ::
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for Books by if books are published or not"""
|
||||
published = MethodFilter()
|
||||
|
||||
def filter_published(self, queryset, value):
|
||||
if value:
|
||||
return queryset.filter(published__isnull=False)
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['published']
|
||||
|
||||
Core Arguments
|
||||
--------------
|
||||
|
||||
These arguments apply to the filters described above.
|
||||
|
||||
``name``
|
||||
~~~~~~~~
|
||||
|
||||
|
@ -242,7 +274,7 @@ to the widgets that are included with Django that you can use there are
|
|||
additional ones that django-filter provides which may be useful:
|
||||
|
||||
* ``django_filters.widgets.LinkWidget`` -- this displays the options in a
|
||||
mannner similar to the way the Django Admin does, as a series of links.
|
||||
manner similar to the way the Django Admin does, as a series of links.
|
||||
The link for the selected option will have ``class="selected"``.
|
||||
|
||||
``action``
|
||||
|
@ -250,7 +282,8 @@ additional ones that django-filter provides which may be useful:
|
|||
|
||||
An optional callable that tells the filter how to handle the queryset. It
|
||||
recieves a ``QuerySet`` and the value to filter on and should return a
|
||||
``Queryset`` that is filtered appropriately.
|
||||
``Queryset`` that is filtered appropriately. `action` will default to
|
||||
``filter_{value-of-name-attribute}``
|
||||
|
||||
``lookup_type``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
@ -274,9 +307,11 @@ queryset. This option can be used to eliminate duplicate results when using filt
|
|||
A boolean value that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset.
|
||||
Defaults to ``False``.
|
||||
|
||||
ModelChoiceFilter and ModelMultipleChoiceFilter
|
||||
ModelChoiceFilter and ModelMultipleChoiceFilter arguments
|
||||
--------------
|
||||
|
||||
These arguments apply specifically to ModelChoiceFiler and
|
||||
ModelMultipleChoiceFilter only.
|
||||
|
||||
``queryset``
|
||||
~~~~~~~~~~~
|
||||
|
|
|
@ -8,7 +8,7 @@ arguments.
|
|||
~~~~~~~~~~~~~~
|
||||
|
||||
This widget renders each option as a link, instead of an actual <input>. It has
|
||||
one method that you can overide for additional customizability.
|
||||
one method that you can override for additional customizability.
|
||||
``option_string()`` should return a string with 3 Python keyword argument
|
||||
placeholders::
|
||||
|
||||
|
@ -17,3 +17,12 @@ placeholders::
|
|||
2. ``query_string``: This is the query string for use in the ``href``
|
||||
option on the ``<a>`` element.
|
||||
3. ``label``: This is the text to be displayed to the user.
|
||||
|
||||
``BooleanWidget``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This widget converts its input into Python's True/False values. It will convert
|
||||
all case variations of ``True`` and ``False`` into the internal Python values.
|
||||
To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``::
|
||||
|
||||
active = BooleanFilter(widget=BooleanWidget())
|
||||
|
|
|
@ -268,10 +268,10 @@ the ``form`` option on a ``ModelAdmin.``
|
|||
Group fields with ``together``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The inner ``Meta`` class also takes an optional ``together`` argument. This
|
||||
is a list of lists, each containing field names. For convenience can be a
|
||||
single list/tuple when dealing with a single set of fields. Fields within a
|
||||
field set must either be all or none present in the request for
|
||||
The inner ``Meta`` class also takes an optional ``together`` argument. This
|
||||
is a list of lists, each containing field names. For convenience can be a
|
||||
single list/tuple when dealing with a single set of fields. Fields within a
|
||||
field set must either be all or none present in the request for
|
||||
``FilterSet.form`` to be valid::
|
||||
|
||||
import django_filters
|
||||
|
@ -365,3 +365,33 @@ to it as the class based view::
|
|||
|
||||
The needed template and its context variables will also be the same as the
|
||||
class-based view above.
|
||||
|
||||
Custom lookup types
|
||||
-------------------
|
||||
|
||||
By default django-filter uses Django's ORM built-in field lookups. If you want to globally accept specific lookups you can do the following:
|
||||
|
||||
from django_filters import filters
|
||||
|
||||
filters.LOOKUP_TYPES = ['gt', 'gte', 'lt', 'lte', 'custom_lookup_type']
|
||||
|
||||
Choices help text
|
||||
-----------------
|
||||
|
||||
If you want the ``ChoiceField`` created from `LOOKUP_TYPES` to have human-friendly options you can do the following:
|
||||
|
||||
from django_filters import filters
|
||||
|
||||
filters.LOOKUP_TYPES = [
|
||||
('', '---------'),
|
||||
('exact', 'Is equal to'),
|
||||
('not_exact', 'Is not equal to'),
|
||||
('lt', 'Lesser than'),
|
||||
('gt', 'Greater than'),
|
||||
('gte', 'Greater than or equal to'),
|
||||
('lte', 'Lesser than or equal to'),
|
||||
('startswith', 'Starts with'),
|
||||
('endswith', 'Ends with'),
|
||||
('contains', 'Contains'),
|
||||
('not_contains', 'Does not contain'),
|
||||
]
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
bumpversion
|
||||
twine==1.5.0
|
||||
wheel==0.24.0
|
||||
coverage==3.7.1
|
|
@ -1,4 +1,4 @@
|
|||
coverage==3.7.1
|
||||
django-discover-runner
|
||||
mock
|
||||
coverage==3.7.1
|
||||
sphinx-autobuild
|
||||
|
|
23
runshell.py
23
runshell.py
|
@ -1,29 +1,12 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
if not settings.configured:
|
||||
settings.configure(
|
||||
DATABASES={
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
},
|
||||
INSTALLED_APPS=(
|
||||
'django_filters',
|
||||
'tests',
|
||||
),
|
||||
ROOT_URLCONF=None,
|
||||
USE_TZ=True,
|
||||
SECRET_KEY='foobar'
|
||||
)
|
||||
|
||||
|
||||
def runshell():
|
||||
call_command('syncdb', interactive=False)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
|
||||
execute_from_command_line(sys.argv[:1] + ['migrate', '--noinput', '-v', '0'])
|
||||
argv = sys.argv[:1] + ['shell'] + sys.argv[1:]
|
||||
execute_from_command_line(argv)
|
||||
|
||||
|
|
30
runtests.py
30
runtests.py
|
@ -1,37 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from django import VERSION
|
||||
from django.conf import settings
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
if not settings.configured:
|
||||
test_runners_args = {}
|
||||
if VERSION < (1, 6):
|
||||
test_runners_args = {
|
||||
'TEST_RUNNER': 'discover_runner.DiscoverRunner',
|
||||
}
|
||||
settings.configure(
|
||||
DATABASES={
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
},
|
||||
INSTALLED_APPS=(
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.auth',
|
||||
'django_filters',
|
||||
'tests',
|
||||
),
|
||||
ROOT_URLCONF=None,
|
||||
USE_TZ=True,
|
||||
SECRET_KEY='foobar',
|
||||
SILENCED_SYSTEM_CHECKS=['1_7.W001'],
|
||||
**test_runners_args
|
||||
)
|
||||
|
||||
|
||||
def runtests():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
|
||||
argv = sys.argv[:1] + ['test'] + sys.argv[1:]
|
||||
execute_from_command_line(argv)
|
||||
|
||||
|
|
14
setup.py
14
setup.py
|
@ -6,11 +6,17 @@ f = open('README.rst')
|
|||
readme = f.read()
|
||||
f.close()
|
||||
|
||||
version = '0.11.0'
|
||||
version = '0.12.0'
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
os.system("python setup.py sdist upload")
|
||||
os.system("python setup.py bdist_wheel upload")
|
||||
if os.system("pip freeze | grep wheel"):
|
||||
print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
|
||||
sys.exit()
|
||||
if os.system("pip freeze | grep twine"):
|
||||
print("twine not installed.\nUse `pip install twine`.\nExiting.")
|
||||
sys.exit()
|
||||
os.system("python setup.py sdist bdist_wheel")
|
||||
os.system("twine upload dist/*")
|
||||
print("You probably want to also tag the version now:")
|
||||
print(" git tag -a %s -m 'version %s'" % (version, version))
|
||||
print(" git push --tags")
|
||||
|
@ -39,12 +45,12 @@ setup(
|
|||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Framework :: Django',
|
||||
],
|
||||
include_package_data=True,
|
||||
|
|
|
@ -31,10 +31,10 @@ class SubnetMaskField(models.Field):
|
|||
models.Field.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "IPAddressField"
|
||||
return "GenericIPAddressField"
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.IPAddressField}
|
||||
defaults = {'form_class': forms.GenericIPAddressField}
|
||||
defaults.update(kwargs)
|
||||
return super(SubnetMaskField, self).formfield(**defaults)
|
||||
|
||||
|
@ -108,7 +108,7 @@ class Restaurant(Place):
|
|||
|
||||
|
||||
class NetworkSetting(models.Model):
|
||||
ip = models.IPAddressField()
|
||||
ip = models.GenericIPAddressField()
|
||||
mask = SubnetMaskField()
|
||||
|
||||
|
||||
|
@ -179,3 +179,6 @@ class Business(models.Model):
|
|||
through=HiredWorker,
|
||||
related_name='employers')
|
||||
|
||||
|
||||
class UUIDTestModel(models.Model):
|
||||
uuid = models.UUIDField()
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
}
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.auth',
|
||||
'django_filters',
|
||||
'tests',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'tests.urls'
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
SECRET_KEY = 'foobar'
|
||||
|
||||
TEMPLATES = [{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
}]
|
||||
|
||||
|
||||
SILENCED_SYSTEM_CHECKS = [
|
||||
'1_7.W001', # Unset MIDDLEWARE_CLASSES warning
|
||||
]
|
|
@ -1,23 +1,20 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime, time
|
||||
from datetime import datetime, time, timedelta, tzinfo
|
||||
import decimal
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (2, 7):
|
||||
import unittest
|
||||
else: # pragma: nocover
|
||||
from django.utils import unittest # noqa
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from django_filters.widgets import RangeWidget
|
||||
from django_filters.fields import (
|
||||
RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)
|
||||
|
||||
|
||||
def to_d(float_value):
|
||||
return decimal.Decimal('%.2f' % float_value)
|
||||
|
||||
|
@ -120,13 +117,51 @@ class LookupTypeFieldTests(TestCase):
|
|||
|
||||
|
||||
class IsoDateTimeFieldTests(TestCase):
|
||||
reference_str = "2015-07-19T13:34:51.759"
|
||||
reference_dt = datetime(2015, 7, 19, 13, 34, 51, 759000)
|
||||
|
||||
def test_datetime_string_is_parsed(self):
|
||||
f = IsoDateTimeField()
|
||||
d = f.strptime("2015-07-19T13:34:51.759", IsoDateTimeField.ISO_8601)
|
||||
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
def test_datetime_string_with_timezone_is_parsed(self):
|
||||
f = IsoDateTimeField()
|
||||
d = f.strptime("2015-07-19T13:34:51.759+01:00", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
def test_datetime_zulu(self):
|
||||
f = IsoDateTimeField()
|
||||
d = f.strptime(self.reference_str + "Z", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
def test_datetime_timezone_awareness(self):
|
||||
# parsed datetimes should obey USE_TZ
|
||||
f = IsoDateTimeField()
|
||||
r = make_aware(self.reference_dt, f.default_timezone)
|
||||
|
||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d.tzinfo, tzinfo))
|
||||
self.assertEqual(d, r + r.utcoffset() - d.utcoffset())
|
||||
|
||||
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d.tzinfo, tzinfo))
|
||||
self.assertEqual(d, r)
|
||||
|
||||
@override_settings(USE_TZ=False)
|
||||
def test_datetime_timezone_naivety(self):
|
||||
# parsed datetimes should obey USE_TZ
|
||||
f = IsoDateTimeField()
|
||||
r = self.reference_dt.replace()
|
||||
|
||||
# It's necessary to override this here, since the field class is parsed
|
||||
# when USE_TZ = True.
|
||||
f.default_timezone = None
|
||||
|
||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(d.tzinfo is None)
|
||||
self.assertEqual(d, r - timedelta(hours=1))
|
||||
|
||||
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(d.tzinfo is None)
|
||||
self.assertEqual(d, r)
|
||||
|
|
|
@ -3,12 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import datetime
|
||||
import mock
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (2, 7):
|
||||
import unittest
|
||||
else: # pragma: nocover
|
||||
from django.utils import unittest # noqa
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils import six
|
||||
|
@ -1331,4 +1326,3 @@ class MiscFilterSetTests(TestCase):
|
|||
f = F({'status': '2'}, queryset=qs)
|
||||
self.assertEqual(len(f.qs), 2)
|
||||
self.assertEqual(f.count(), 2)
|
||||
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import date, datetime, time
|
||||
from datetime import date, time, timedelta
|
||||
import mock
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (2, 7):
|
||||
import unittest
|
||||
else: # pragma: nocover
|
||||
from django.utils import unittest # noqa
|
||||
|
||||
from datetime import timedelta
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
from django_filters import filters
|
||||
from django_filters.fields import (
|
||||
Lookup,
|
||||
RangeField,
|
||||
|
@ -40,6 +34,7 @@ from django_filters.filters import (
|
|||
DateFromToRangeFilter,
|
||||
TimeRangeFilter,
|
||||
AllValuesFilter,
|
||||
UUIDFilter,
|
||||
LOOKUP_TYPES)
|
||||
|
||||
from tests.models import Book, User
|
||||
|
@ -189,7 +184,7 @@ class FilterTests(TestCase):
|
|||
qs = mock.Mock(spec=['filter', 'distinct'])
|
||||
f = Filter(name='somefield', distinct=True)
|
||||
f.filter(qs, 'value')
|
||||
result = qs.distinct.assert_called_once()
|
||||
result = qs.distinct.assert_called_once_with()
|
||||
self.assertNotEqual(qs, result)
|
||||
|
||||
|
||||
|
@ -201,6 +196,14 @@ class CharFilterTests(TestCase):
|
|||
self.assertIsInstance(field, forms.CharField)
|
||||
|
||||
|
||||
class UUIDFilterTests(TestCase):
|
||||
|
||||
def test_default_field(self):
|
||||
f = UUIDFilter()
|
||||
field = f.field
|
||||
self.assertIsInstance(field, forms.UUIDField)
|
||||
|
||||
|
||||
class BooleanFilterTests(TestCase):
|
||||
|
||||
def test_default_field(self):
|
||||
|
@ -212,14 +215,14 @@ class BooleanFilterTests(TestCase):
|
|||
qs = mock.Mock(spec=['filter'])
|
||||
f = BooleanFilter(name='somefield')
|
||||
result = f.filter(qs, True)
|
||||
qs.filter.assert_called_once_with(somefield=True)
|
||||
qs.filter.assert_called_once_with(somefield__exact=True)
|
||||
self.assertNotEqual(qs, result)
|
||||
|
||||
def test_filtering_exclude(self):
|
||||
qs = mock.Mock(spec=['exclude'])
|
||||
f = BooleanFilter(name='somefield', exclude=True)
|
||||
result = f.filter(qs, True)
|
||||
qs.exclude.assert_called_once_with(somefield=True)
|
||||
qs.exclude.assert_called_once_with(somefield__exact=True)
|
||||
self.assertNotEqual(qs, result)
|
||||
|
||||
@unittest.expectedFailure
|
||||
|
@ -237,6 +240,13 @@ class BooleanFilterTests(TestCase):
|
|||
self.assertListEqual(qs.method_calls, [])
|
||||
self.assertEqual(qs, result)
|
||||
|
||||
def test_filtering_lookup_type(self):
|
||||
qs = mock.Mock(spec=['filter'])
|
||||
f = BooleanFilter(name='somefield', lookup_type='isnull')
|
||||
result = f.filter(qs, True)
|
||||
qs.filter.assert_called_once_with(somefield__isnull=True)
|
||||
self.assertNotEqual(qs, result)
|
||||
|
||||
|
||||
class ChoiceFilterTests(TestCase):
|
||||
|
||||
|
@ -759,3 +769,30 @@ class AllValuesFilterTests(TestCase):
|
|||
f.model = mocked
|
||||
field = f.field
|
||||
self.assertIsInstance(field, forms.ChoiceField)
|
||||
|
||||
|
||||
class LookupTypesTests(TestCase):
|
||||
def test_custom_lookup_types(self):
|
||||
filters.LOOKUP_TYPES = [
|
||||
('', '---------'),
|
||||
('exact', 'Is equal to'),
|
||||
('not_exact', 'Is not equal to'),
|
||||
('lt', 'Lesser than'),
|
||||
('gt', 'Greater than'),
|
||||
('gte', 'Greater than or equal to'),
|
||||
('lte', 'Lesser than or equal to'),
|
||||
('startswith', 'Starts with'),
|
||||
('endswith', 'Ends with'),
|
||||
('contains', 'Contains'),
|
||||
('not_contains', 'Does not contain'),
|
||||
]
|
||||
|
||||
f = Filter(lookup_type=None)
|
||||
field = f.field
|
||||
choice_field = field.fields[1]
|
||||
choices = choice_field.choices
|
||||
|
||||
self.assertIsInstance(field, LookupTypeField)
|
||||
self.assertEqual(choices, filters.LOOKUP_TYPES)
|
||||
self.assertEqual(choices[1][0], 'exact')
|
||||
self.assertEqual(choices[1][1], 'Is equal to')
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import mock
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (2, 7):
|
||||
import unittest
|
||||
else: # pragma: nocover
|
||||
from django.utils import unittest # noqa
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
|
||||
|
@ -19,6 +15,7 @@ from django_filters.filters import NumberFilter
|
|||
from django_filters.filters import ChoiceFilter
|
||||
from django_filters.filters import ModelChoiceFilter
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filters import UUIDFilter
|
||||
|
||||
from .models import User
|
||||
from .models import AdminUser
|
||||
|
@ -35,6 +32,7 @@ from .models import DirectedNode
|
|||
from .models import Worker
|
||||
from .models import HiredWorker
|
||||
from .models import Business
|
||||
from .models import UUIDTestModel
|
||||
|
||||
|
||||
def checkItemsEqual(L1, L2):
|
||||
|
@ -43,6 +41,7 @@ def checkItemsEqual(L1, L2):
|
|||
"""
|
||||
return len(L1) == len(L2) and sorted(L1) == sorted(L2)
|
||||
|
||||
|
||||
class HelperMethodsTests(TestCase):
|
||||
|
||||
@unittest.skip('todo')
|
||||
|
@ -80,7 +79,7 @@ class DbFieldDefaultFiltersTests(TestCase):
|
|||
models.FilePathField,
|
||||
models.FloatField,
|
||||
models.IntegerField,
|
||||
models.IPAddressField,
|
||||
models.GenericIPAddressField,
|
||||
models.NullBooleanField,
|
||||
models.PositiveIntegerField,
|
||||
models.PositiveSmallIntegerField,
|
||||
|
@ -92,6 +91,7 @@ class DbFieldDefaultFiltersTests(TestCase):
|
|||
models.ForeignKey,
|
||||
models.OneToOneField,
|
||||
models.ManyToManyField,
|
||||
models.UUIDField,
|
||||
]
|
||||
msg = "%s expected to be found in FILTER_FOR_DBFIELD_DEFAULTS"
|
||||
|
||||
|
@ -102,7 +102,6 @@ class DbFieldDefaultFiltersTests(TestCase):
|
|||
to_check = [
|
||||
models.Field,
|
||||
models.BigIntegerField,
|
||||
models.GenericIPAddressField,
|
||||
models.FileField,
|
||||
models.ImageField,
|
||||
]
|
||||
|
@ -120,6 +119,12 @@ class FilterSetFilterForFieldTests(TestCase):
|
|||
self.assertIsInstance(result, CharFilter)
|
||||
self.assertEqual(result.name, 'username')
|
||||
|
||||
def test_filter_found_for_uuidfield(self):
|
||||
f = UUIDTestModel._meta.get_field('uuid')
|
||||
result = FilterSet.filter_for_field(f, 'uuid')
|
||||
self.assertIsInstance(result, UUIDFilter)
|
||||
self.assertEqual(result.name, 'uuid')
|
||||
|
||||
def test_filter_found_for_autofield(self):
|
||||
f = User._meta.get_field('id')
|
||||
result = FilterSet.filter_for_field(f, 'id')
|
||||
|
@ -183,7 +188,7 @@ class FilterSetFilterForFieldTests(TestCase):
|
|||
class FilterSetFilterForReverseFieldTests(TestCase):
|
||||
|
||||
def test_reverse_o2o_relationship(self):
|
||||
f = Account._meta.get_field_by_name('profile')[0]
|
||||
f = Account._meta.get_field('profile')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'profile')
|
||||
self.assertIsInstance(result, ModelChoiceFilter)
|
||||
self.assertEqual(result.name, 'profile')
|
||||
|
@ -192,7 +197,7 @@ class FilterSetFilterForReverseFieldTests(TestCase):
|
|||
self.assertEqual(result.extra['queryset'].model, Profile)
|
||||
|
||||
def test_reverse_fk_relationship(self):
|
||||
f = User._meta.get_field_by_name('comments')[0]
|
||||
f = User._meta.get_field('comments')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'comments')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'comments')
|
||||
|
@ -201,7 +206,7 @@ class FilterSetFilterForReverseFieldTests(TestCase):
|
|||
self.assertEqual(result.extra['queryset'].model, Comment)
|
||||
|
||||
def test_reverse_m2m_relationship(self):
|
||||
f = Book._meta.get_field_by_name('lovers')[0]
|
||||
f = Book._meta.get_field('lovers')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'lovers')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'lovers')
|
||||
|
@ -210,7 +215,7 @@ class FilterSetFilterForReverseFieldTests(TestCase):
|
|||
self.assertEqual(result.extra['queryset'].model, User)
|
||||
|
||||
def test_reverse_non_symmetrical_selfref_m2m_field(self):
|
||||
f = DirectedNode._meta.get_field_by_name('inbound_nodes')[0]
|
||||
f = DirectedNode._meta.get_field('inbound_nodes')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'inbound_nodes')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'inbound_nodes')
|
||||
|
@ -219,7 +224,7 @@ class FilterSetFilterForReverseFieldTests(TestCase):
|
|||
self.assertEqual(result.extra['queryset'].model, DirectedNode)
|
||||
|
||||
def test_reverse_m2m_field_with_through_model(self):
|
||||
f = Worker._meta.get_field_by_name('employers')[0]
|
||||
f = Worker._meta.get_field('employers')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'employers')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'employers')
|
||||
|
@ -558,20 +563,30 @@ class FilterSetOrderingTests(TestCase):
|
|||
self.assertQuerysetEqual(
|
||||
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
|
||||
|
||||
@unittest.skip('todo')
|
||||
def test_ordering_uses_filter_name(self):
|
||||
class F(FilterSet):
|
||||
account = CharFilter(name='username')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['account', 'status']
|
||||
order_by = True
|
||||
|
||||
f = F({'o': 'username'}, queryset=self.qs)
|
||||
f = F({'o': 'account'}, queryset=self.qs)
|
||||
self.assertQuerysetEqual(
|
||||
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
|
||||
|
||||
def test_reverted_ordering_uses_filter_name(self):
|
||||
class F(FilterSet):
|
||||
account = CharFilter(name='username')
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['account', 'status']
|
||||
order_by = True
|
||||
|
||||
f = F({'o': '-account'}, queryset=self.qs)
|
||||
self.assertQuerysetEqual(
|
||||
f.qs, ['jacob', 'carl', 'alex', 'aaron'], lambda o: o.username)
|
||||
|
||||
def test_ordering_with_overridden_field_name(self):
|
||||
"""
|
||||
Set the `order_by_field` on the queryset and ensure that the
|
||||
|
@ -642,14 +657,14 @@ class FilterSetTogetherTests(TestCase):
|
|||
self.alex = User.objects.create(username='alex', status=1)
|
||||
self.jacob = User.objects.create(username='jacob', status=2)
|
||||
self.qs = User.objects.all().order_by('id')
|
||||
|
||||
|
||||
def test_fields_set(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'status', 'is_active', 'first_name']
|
||||
together = [
|
||||
('username', 'status'),
|
||||
('username', 'status'),
|
||||
('first_name', 'is_active'),
|
||||
]
|
||||
|
||||
|
@ -660,14 +675,14 @@ class FilterSetTogetherTests(TestCase):
|
|||
f = F({'username': 'alex', 'status': 1}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 1)
|
||||
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
|
||||
|
||||
|
||||
def test_single_fields_set(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'status']
|
||||
together = ['username', 'status']
|
||||
|
||||
|
||||
f = F({}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 2)
|
||||
f = F({'username': 'alex'}, queryset=self.qs)
|
||||
|
|
|
@ -199,7 +199,7 @@ class FilterSetFormTests(TestCase):
|
|||
self.assertEqual(f.fields['o'].choices,
|
||||
[('username', 'Account'), ('-username', 'Account (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
|
||||
|
||||
def test_ordering_uses_implicit_filter_name(self):
|
||||
def test_ordering_uses_explicit_filter_name(self):
|
||||
class F(FilterSet):
|
||||
account = CharFilter(name='username')
|
||||
|
||||
|
@ -210,7 +210,7 @@ class FilterSetFormTests(TestCase):
|
|||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['o'].choices,
|
||||
[('username', 'Account'), ('-username', 'Account (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
|
||||
[('account', 'Account'), ('-account', 'Account (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
|
||||
|
||||
def test_ordering_with_overridden_field_name(self):
|
||||
"""
|
||||
|
@ -272,4 +272,3 @@ class FilterSetFormTests(TestCase):
|
|||
f = F().form
|
||||
self.assertEqual(
|
||||
f.fields['o'].choices, [('status', 'Current status')])
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import absolute_import
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from django_filters.views import FilterView
|
||||
|
@ -11,8 +11,8 @@ from django_filters.filterset import FilterSet, filterset_factory
|
|||
from .models import Book
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.urls')
|
||||
class GenericViewTestCase(TestCase):
|
||||
urls = 'tests.urls'
|
||||
|
||||
def setUp(self):
|
||||
Book.objects.create(
|
||||
|
@ -78,4 +78,3 @@ class GenericFunctionalViewTests(GenericViewTestCase):
|
|||
for b in ['Ender's Game', 'Rainbow Six']:
|
||||
self.assertNotContains(response, b)
|
||||
self.assertContains(response, 'Snowcrash')
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||
from django.test import TestCase
|
||||
from django.forms import TextInput, Select
|
||||
|
||||
from django_filters.widgets import BooleanWidget
|
||||
from django_filters.widgets import RangeWidget
|
||||
from django_filters.widgets import LinkWidget
|
||||
from django_filters.widgets import LookupTypeWidget
|
||||
|
@ -135,3 +136,30 @@ class RangeWidgetTests(TestCase):
|
|||
-
|
||||
<input type="text" name="price_1" value="9.99" />""")
|
||||
|
||||
def test_widget_attributes(self):
|
||||
w = RangeWidget(attrs={'type': 'date'})
|
||||
self.assertEqual(len(w.widgets), 2)
|
||||
self.assertHTMLEqual(w.render('date', ''), """
|
||||
<input type="date" name="date_0" />
|
||||
-
|
||||
<input type="date" name="date_1" />""")
|
||||
|
||||
|
||||
class BooleanWidgetTests(TestCase):
|
||||
"""
|
||||
"""
|
||||
def test_widget_value_from_datadict(self):
|
||||
"""
|
||||
"""
|
||||
w = BooleanWidget()
|
||||
|
||||
trueActive = {'active': 'true'}
|
||||
result = w.value_from_datadict(trueActive, {}, 'active')
|
||||
self.assertEqual(result, True)
|
||||
|
||||
falseActive = {'active': 'false'}
|
||||
result = w.value_from_datadict(falseActive, {}, 'active')
|
||||
self.assertEqual(result, False)
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'active')
|
||||
self.assertEqual(result, None)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import patterns
|
||||
from django.conf.urls import url
|
||||
|
||||
from django_filters.views import FilterView
|
||||
from django_filters.views import FilterView, object_filter
|
||||
from .models import Book
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^books-legacy/$',
|
||||
'django_filters.views.object_filter', {'model': Book}),
|
||||
(r'^books/$', FilterView.as_view(model=Book)),
|
||||
)
|
||||
urlpatterns = [
|
||||
url(r'^books-legacy/$', object_filter, {'model': Book}),
|
||||
url(r'^books/$', FilterView.as_view(model=Book)),
|
||||
]
|
||||
|
|
26
tox.ini
26
tox.ini
|
@ -1,17 +1,25 @@
|
|||
[tox]
|
||||
envlist =
|
||||
{py26,py27}-django14,
|
||||
{py26,py27,py32,py33,py34}-django{15,16},
|
||||
{py27,py32,py33,py34}-django{17,18}
|
||||
{py27,py32,py33,py34}-django{17,18},
|
||||
{py27,py34,py35}-django19,
|
||||
{py27,py34,py35}-django-latest
|
||||
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py27: python2.7
|
||||
py32: python3.2
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
py35: python3.5
|
||||
|
||||
|
||||
commands = ./runtests.py
|
||||
setenv =
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
deps =
|
||||
django14: Django==1.4.11 # Should track minimum supported
|
||||
django15: Django==1.5.6 # Should track minimum supported
|
||||
django16: Django==1.6.3 # Should track minimum supported
|
||||
django17: Django==1.7.2 # Should track maximum supported
|
||||
django18: Django==1.8.1 # Should track maximum supported
|
||||
-rrequirements/test.txt
|
||||
django17: django>=1.7.0,<1.8.0
|
||||
django18: django>=1.8.0,<1.9.0
|
||||
django19: https://www.djangoproject.com/download/1.9a1/tarball/
|
||||
django-latest: https://github.com/django/django/archive/master.tar.gz
|
||||
-rrequirements/test.txt
|
||||
|
|
Loading…
Reference in New Issue