commit
a96f93ac86
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 0.15.0
|
||||
current_version = 0.15.1
|
||||
commit = False
|
||||
tag = False
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
||||
|
|
11
CHANGES.rst
11
CHANGES.rst
|
@ -1,3 +1,14 @@
|
|||
Version 0.15.1 (2016-09-28)
|
||||
---------------------------
|
||||
|
||||
A couple of quick bug fixes:
|
||||
|
||||
* #496 OrderingFilter not working with Select widget
|
||||
|
||||
* #498 DRF Backend Templates not loading
|
||||
|
||||
|
||||
|
||||
Version 0.15.0 (2016-09-20)
|
||||
---------------------------
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import absolute_import
|
|||
from .filterset import FilterSet
|
||||
from .filters import *
|
||||
|
||||
__version__ = '0.15.0'
|
||||
__version__ = '0.15.1'
|
||||
|
||||
|
||||
def parse_version(version):
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.encoding import force_str
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .utils import handle_timezone
|
||||
from .widgets import RangeWidget, LookupTypeWidget, CSVWidget
|
||||
from .widgets import RangeWidget, LookupTypeWidget, CSVWidget, BaseCSVWidget
|
||||
|
||||
|
||||
class RangeField(forms.MultiValueField):
|
||||
|
@ -129,7 +129,28 @@ class BaseCSVField(forms.Field):
|
|||
pass
|
||||
|
||||
"""
|
||||
widget = CSVWidget
|
||||
base_widget_class = BaseCSVWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
widget = kwargs.get('widget') or self.widget
|
||||
kwargs['widget'] = self._get_widget_class(widget)
|
||||
|
||||
super(BaseCSVField, self).__init__(*args, **kwargs)
|
||||
|
||||
def _get_widget_class(self, widget):
|
||||
# passthrough, allows for override
|
||||
if isinstance(widget, BaseCSVWidget) or (
|
||||
isinstance(widget, type) and
|
||||
issubclass(widget, BaseCSVWidget)):
|
||||
return widget
|
||||
|
||||
# complain since we are unable to reconstruct widget instances
|
||||
assert isinstance(widget, type), \
|
||||
"'%s.widget' must be a widget class, not %s." \
|
||||
% (self.__class__.__name__, repr(widget))
|
||||
|
||||
bases = (self.base_widget_class, widget, )
|
||||
return type(str('CSV%s' % widget.__name__), bases, {})
|
||||
|
||||
def clean(self, value):
|
||||
if value is None:
|
||||
|
@ -138,6 +159,10 @@ class BaseCSVField(forms.Field):
|
|||
|
||||
|
||||
class BaseRangeField(BaseCSVField):
|
||||
# Force use of text input, as range must always have two inputs. A date
|
||||
# input would only allow a user to input one value and would always fail.
|
||||
widget = CSVWidget
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_values': _('Range query expects two values.')
|
||||
}
|
||||
|
|
|
@ -1,22 +1,45 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.template import loader
|
||||
from django.template import Template, TemplateDoesNotExist, loader
|
||||
from rest_framework.compat import template_render
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
|
||||
from .. import compat
|
||||
from . import filterset
|
||||
|
||||
|
||||
CRISPY_TEMPLATE = """
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
{% crispy filter.form %}
|
||||
"""
|
||||
|
||||
|
||||
FILTER_TEMPLATE = """
|
||||
{% load i18n %}
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
<form class="form" action="" method="get">
|
||||
{{ filter.form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
"""
|
||||
|
||||
|
||||
if compat.is_crispy:
|
||||
filter_template = 'django_filters/rest_framework/crispy_form.html'
|
||||
template_path = 'django_filters/rest_framework/crispy_form.html'
|
||||
template_default = Template(CRISPY_TEMPLATE)
|
||||
|
||||
else:
|
||||
filter_template = 'django_filters/rest_framework/form.html'
|
||||
template_path = 'django_filters/rest_framework/form.html'
|
||||
template_default = Template(FILTER_TEMPLATE)
|
||||
|
||||
|
||||
class DjangoFilterBackend(BaseFilterBackend):
|
||||
default_filter_set = filterset.FilterSet
|
||||
template = filter_template
|
||||
template = template_path
|
||||
|
||||
def get_filter_class(self, view, queryset=None):
|
||||
"""
|
||||
|
@ -57,8 +80,12 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
if not filter_class:
|
||||
return None
|
||||
filter_instance = filter_class(request.query_params, queryset=queryset)
|
||||
context = {
|
||||
|
||||
try:
|
||||
template = loader.get_template(self.template)
|
||||
except TemplateDoesNotExist:
|
||||
template = template_default
|
||||
|
||||
return template_render(template, context={
|
||||
'filter': filter_instance
|
||||
}
|
||||
template = loader.get_template(self.template)
|
||||
return compat.template_render(template, context)
|
||||
})
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
{% crispy filter.form %}
|
|
@ -1,6 +0,0 @@
|
|||
{% load i18n %}
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
<form class="form" action="" method="get">
|
||||
{{ filter.form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
||||
</form>
|
|
@ -138,12 +138,12 @@ class BooleanWidget(forms.Select):
|
|||
}.get(value, None)
|
||||
|
||||
|
||||
class CSVWidget(forms.TextInput):
|
||||
class BaseCSVWidget(forms.Widget):
|
||||
def _isiterable(self, value):
|
||||
return isinstance(value, Iterable) and not isinstance(value, string_types)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = super(CSVWidget, self).value_from_datadict(data, files, name)
|
||||
value = super(BaseCSVWidget, self).value_from_datadict(data, files, name)
|
||||
|
||||
if value is not None:
|
||||
if value == '': # empty value should parse as an empty list
|
||||
|
@ -152,8 +152,22 @@ class CSVWidget(forms.TextInput):
|
|||
return None
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
if self._isiterable(value):
|
||||
value = [force_text(format_value(self, v)) for v in value]
|
||||
value = ','.join(list(value))
|
||||
if not self._isiterable(value):
|
||||
value = [value]
|
||||
|
||||
return super(CSVWidget, self).render(name, value, attrs)
|
||||
if len(value) <= 1:
|
||||
# delegate to main widget (Select, etc...) if not multiple values
|
||||
value = value[0] if value else value
|
||||
return super(BaseCSVWidget, self).render(name, value, attrs)
|
||||
|
||||
# if we have multiple values, we need to force render as a text input
|
||||
# (otherwise, the additional values are lost)
|
||||
surrogate = forms.TextInput()
|
||||
value = [force_text(format_value(surrogate, v)) for v in value]
|
||||
value = ','.join(list(value))
|
||||
|
||||
return surrogate.render(name, value, attrs)
|
||||
|
||||
|
||||
class CSVWidget(BaseCSVWidget, forms.TextInput):
|
||||
pass
|
||||
|
|
|
@ -48,9 +48,9 @@ copyright = u'2013, Alex Gaynor and others.'
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.15.0'
|
||||
version = '0.15.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.15.0'
|
||||
release = '0.15.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
|
@ -674,7 +674,7 @@ two additional arguments that are used to build the ordering choices.
|
|||
('username', 'account'),
|
||||
('first_name', 'first_name'),
|
||||
('last_name', 'last_name'),
|
||||
},
|
||||
),
|
||||
|
||||
# labels do not need to retain order
|
||||
field_labels={
|
||||
|
@ -686,7 +686,7 @@ two additional arguments that are used to build the ordering choices.
|
|||
model = User
|
||||
fields = ['first_name', 'last_name']
|
||||
|
||||
>>> UserFilter().filter['o'].field.choices
|
||||
>>> UserFilter().filters['o'].field.choices
|
||||
[
|
||||
('account', 'User account'),
|
||||
('-account', 'User account (descending)'),
|
||||
|
|
2
setup.py
2
setup.py
|
@ -6,7 +6,7 @@ f = open('README.rst')
|
|||
readme = f.read()
|
||||
f.close()
|
||||
|
||||
version = '0.15.0'
|
||||
version = '0.15.1'
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
if os.system("pip freeze | grep wheel"):
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'tests.rest_framework.apps.RestFrameworkTestConfig'
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RestFrameworkTestConfig(AppConfig):
|
||||
name = 'tests.rest_framework'
|
||||
label = 'drf_test_app'
|
||||
verbose_name = "Rest Framework Test App"
|
|
@ -0,0 +1 @@
|
|||
Test
|
|
@ -280,6 +280,53 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_html_rendering(self):
|
||||
"""
|
||||
Make sure response renders w/ backend
|
||||
"""
|
||||
view = FilterFieldsRootView.as_view()
|
||||
request = factory.get('/')
|
||||
request.META['HTTP_ACCEPT'] = 'text/html'
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_backend_output(self):
|
||||
"""
|
||||
Ensure backend renders default if template path does not exist
|
||||
"""
|
||||
view = FilterFieldsRootView()
|
||||
backend = view.filter_backends[0]
|
||||
request = view.initialize_request(factory.get('/'))
|
||||
html = backend().to_html(request, view.get_queryset(), view)
|
||||
|
||||
self.assertHTMLEqual(html, """
|
||||
<h2>Field filters</h2>
|
||||
<form class="form" action="" method="get">
|
||||
<p>
|
||||
<label for="id_decimal">Decimal:</label>
|
||||
<input id="id_decimal" name="decimal" step="any" type="number" />
|
||||
<span class="helptext">Filter</span>
|
||||
</p>
|
||||
<p>
|
||||
<label for="id_date">Date:</label>
|
||||
<input id="id_date" name="date" type="text" />
|
||||
<span class="helptext">Filter</span>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
""")
|
||||
|
||||
def test_template_path(self):
|
||||
view = FilterFieldsRootView()
|
||||
|
||||
class Backend(view.filter_backends[0]):
|
||||
template = 'filter_template.html'
|
||||
|
||||
request = view.initialize_request(factory.get('/'))
|
||||
html = Backend().to_html(request, view.get_queryset(), view)
|
||||
|
||||
self.assertHTMLEqual(html, "Test")
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.rest_framework.test_backends')
|
||||
class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
||||
|
|
|
@ -7,12 +7,15 @@ DATABASES = {
|
|||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.auth',
|
||||
'django_filters',
|
||||
'tests',
|
||||
'rest_framework',
|
||||
'tests.rest_framework',
|
||||
'tests',
|
||||
)
|
||||
|
||||
MIDDLEWARE = []
|
||||
|
||||
ROOT_URLCONF = 'tests.urls'
|
||||
|
||||
USE_TZ = True
|
||||
|
@ -25,7 +28,7 @@ TEMPLATES = [{
|
|||
}]
|
||||
|
||||
|
||||
MIDDLEWARE = []
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
# help verify that DEFAULTS is importable from conf.
|
||||
|
|
|
@ -3,14 +3,12 @@ from __future__ import unicode_literals
|
|||
|
||||
from datetime import datetime, time, timedelta, tzinfo
|
||||
import decimal
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils.timezone import make_aware, get_default_timezone
|
||||
|
||||
from django_filters.widgets import RangeWidget
|
||||
from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
|
||||
from django_filters.fields import (
|
||||
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
|
||||
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
|
||||
|
@ -196,6 +194,25 @@ class BaseCSVFieldTests(TestCase):
|
|||
with self.assertRaises(forms.ValidationError):
|
||||
self.field.clean(['a', 'b', 'c'])
|
||||
|
||||
def test_derived_widget(self):
|
||||
with self.assertRaises(AssertionError) as excinfo:
|
||||
BaseCSVField(widget=RangeWidget())
|
||||
|
||||
msg = str(excinfo.exception)
|
||||
self.assertIn("'BaseCSVField.widget' must be a widget class", msg)
|
||||
self.assertIn("RangeWidget", msg)
|
||||
|
||||
widget = CSVWidget()
|
||||
field = BaseCSVField(widget=widget)
|
||||
self.assertIs(field.widget, widget)
|
||||
|
||||
field = BaseCSVField(widget=CSVWidget)
|
||||
self.assertIsInstance(field.widget, CSVWidget)
|
||||
|
||||
field = BaseCSVField(widget=forms.Select)
|
||||
self.assertIsInstance(field.widget, forms.Select)
|
||||
self.assertIsInstance(field.widget, BaseCSVWidget)
|
||||
|
||||
|
||||
class BaseRangeFieldTests(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -6,6 +6,7 @@ import mock
|
|||
import unittest
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import six
|
||||
from django.utils.timezone import now
|
||||
|
@ -24,6 +25,7 @@ from django_filters.filters import DurationFilter
|
|||
from django_filters.filters import MultipleChoiceFilter
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filters import NumberFilter
|
||||
from django_filters.filters import OrderingFilter
|
||||
from django_filters.filters import RangeFilter
|
||||
from django_filters.filters import TimeRangeFilter
|
||||
# from django_filters.widgets import LinkWidget
|
||||
|
@ -1586,6 +1588,46 @@ class CSVFilterTests(TestCase):
|
|||
self.assertEqual(f.qs.count(), 2)
|
||||
|
||||
|
||||
class OrderingFilterTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
User.objects.create(username='alex', status=1)
|
||||
User.objects.create(username='jacob', status=2)
|
||||
User.objects.create(username='aaron', status=2)
|
||||
User.objects.create(username='carl', status=0)
|
||||
|
||||
def test_ordering(self):
|
||||
class F(FilterSet):
|
||||
o = OrderingFilter(
|
||||
fields=('username', )
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
qs = User.objects.all()
|
||||
f = F({'o': 'username'}, queryset=qs)
|
||||
names = f.qs.values_list('username', flat=True)
|
||||
self.assertEqual(list(names), ['aaron', 'alex', 'carl', 'jacob'])
|
||||
|
||||
def test_ordering_with_select_widget(self):
|
||||
class F(FilterSet):
|
||||
o = OrderingFilter(
|
||||
widget=forms.Select,
|
||||
fields=('username', )
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
qs = User.objects.all()
|
||||
f = F({'o': 'username'}, queryset=qs)
|
||||
names = f.qs.values_list('username', flat=True)
|
||||
self.assertEqual(list(names), ['aaron', 'alex', 'carl', 'jacob'])
|
||||
|
||||
|
||||
class MiscFilterSetTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -9,7 +9,7 @@ import warnings
|
|||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters import filters
|
||||
from django_filters import filters, widgets
|
||||
from django_filters.fields import (
|
||||
Lookup,
|
||||
RangeField,
|
||||
|
@ -1097,3 +1097,10 @@ class OrderingFilterTests(TestCase):
|
|||
with self.assertRaises(AssertionError) as ctx:
|
||||
f([0, 1, 2])
|
||||
self.assertEqual(str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.")
|
||||
|
||||
def test_widget(self):
|
||||
f = OrderingFilter()
|
||||
widget = f.field.widget
|
||||
|
||||
self.assertIsInstance(widget, widgets.BaseCSVWidget)
|
||||
self.assertIsInstance(widget, forms.Select)
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.test import TestCase
|
|||
from django.forms import TextInput, Select
|
||||
|
||||
from django_filters.widgets import BooleanWidget
|
||||
from django_filters.widgets import BaseCSVWidget
|
||||
from django_filters.widgets import CSVWidget
|
||||
from django_filters.widgets import RangeWidget
|
||||
from django_filters.widgets import LinkWidget
|
||||
|
@ -182,6 +183,9 @@ class CSVWidgetTests(TestCase):
|
|||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<input type="text" name="price" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', '1'), """
|
||||
<input type="text" name="price" value="1" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', '1,2'), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
|
@ -224,3 +228,54 @@ class CSVWidgetTests(TestCase):
|
|||
|
||||
result = w.value_from_datadict({}, {}, 'price')
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
class CSVSelectTests(TestCase):
|
||||
class CSVSelect(BaseCSVWidget, Select):
|
||||
pass
|
||||
|
||||
def test_widget(self):
|
||||
w = self.CSVSelect(choices=((1, 'a'), (2, 'b')))
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', None),
|
||||
"""
|
||||
<select name="price">
|
||||
<option value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', ''),
|
||||
"""
|
||||
<select name="price">
|
||||
<option value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
""")
|
||||
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', '1'),
|
||||
"""
|
||||
<select name="price">
|
||||
<option selected="selected" value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
""")
|
||||
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', '1,2'),
|
||||
"""
|
||||
<select name="price">
|
||||
<option value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(w.render('price', ['1', '2']), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', [1, 2]), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
|
Loading…
Reference in New Issue