Repository URL to install this package:
|
Version:
1.1.0 ▾
|
django-filter
/
fields.py
|
|---|
from __future__ import absolute_import, unicode_literals
from collections import namedtuple
from datetime import datetime, time
import django
from django import forms
from django.utils.dateparse import parse_datetime
from django.utils.encoding import force_str
from django.utils.translation import ugettext_lazy as _
from .conf import settings
from .utils import handle_timezone
from .widgets import BaseCSVWidget, CSVWidget, LookupTypeWidget, RangeWidget
class RangeField(forms.MultiValueField):
widget = RangeWidget
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 = handle_timezone(
datetime.combine(start_date, time.min),
False
)
if stop_date:
stop_date = handle_timezone(
datetime.combine(stop_date, time.max),
False
)
return slice(start_date, stop_date)
return None
class DateTimeRangeField(RangeField):
def __init__(self, *args, **kwargs):
fields = (
forms.DateTimeField(),
forms.DateTimeField())
super(DateTimeRangeField, self).__init__(fields, *args, **kwargs)
class TimeRangeField(RangeField):
def __init__(self, *args, **kwargs):
fields = (
forms.TimeField(),
forms.TimeField())
super(TimeRangeField, self).__init__(fields, *args, **kwargs)
class Lookup(namedtuple('Lookup', ('value', 'lookup_type'))):
# python nature is test __len__ on tuple types for boolean check
def __len__(self):
if not self.value:
return 0
return 2
class LookupTypeField(forms.MultiValueField):
def __init__(self, field, lookup_choices, *args, **kwargs):
fields = (
field,
forms.ChoiceField(choices=lookup_choices)
)
defaults = {
'widgets': [f.widget for f in fields],
}
widget = LookupTypeWidget(**defaults)
kwargs['widget'] = widget
kwargs['help_text'] = field.help_text
super(LookupTypeField, self).__init__(fields, *args, **kwargs)
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):
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 handle_timezone(parsed)
return super(IsoDateTimeField, self).strptime(value, format)
class BaseCSVField(forms.Field):
"""
Base field for validating CSV types. Value validation is performed by
secondary base classes.
ex::
class IntegerCSVField(BaseCSVField, filters.IntegerField):
pass
"""
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:
return None
return [super(BaseCSVField, self).clean(v) for v in value]
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.')
}
def clean(self, value):
value = super(BaseRangeField, self).clean(value)
if value is not None and len(value) != 2:
raise forms.ValidationError(
self.error_messages['invalid_values'],
code='invalid_values')
return value
class ChoiceIterator(object):
# Emulates the behavior of ModelChoiceIterator, but instead wraps
# the field's _choices iterable.
def __init__(self, field, choices):
self.field = field
self.choices = choices
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
if self.field.null_label is not None:
yield (self.field.null_value, self.field.null_label)
# Python 2 lacks 'yield from'
for choice in self.choices:
yield choice
def __len__(self):
add = 1 if self.field.empty_label is not None else 0
add += 1 if self.field.null_label is not None else 0
return len(self.choices) + add
class ModelChoiceIterator(forms.models.ModelChoiceIterator):
# Extends the base ModelChoiceIterator to add in 'null' choice handling.
# This is a bit verbose since we have to insert the null choice after the
# empty choice, but before the remainder of the choices.
def __iter__(self):
iterable = super(ModelChoiceIterator, self).__iter__()
if self.field.empty_label is not None:
yield next(iterable)
if self.field.null_label is not None:
yield (self.field.null_value, self.field.null_label)
# Python 2 lacks 'yield from'
for value in iterable:
yield value
def __len__(self):
add = 1 if self.field.null_label is not None else 0
return super(ModelChoiceIterator, self).__len__() + add
class ChoiceIteratorMixin(object):
def __init__(self, *args, **kwargs):
self.null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
self.null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
super(ChoiceIteratorMixin, self).__init__(*args, **kwargs)
def _get_choices(self):
if django.VERSION >= (1, 11):
return super(ChoiceIteratorMixin, self)._get_choices()
# HACK: Django < 1.11 does not allow a custom iterator to be provided.
# This code only executes for Model*ChoiceFields.
if hasattr(self, '_choices'):
return self._choices
return self.iterator(self)
def _set_choices(self, value):
super(ChoiceIteratorMixin, self)._set_choices(value)
value = self.iterator(self, self._choices)
self._choices = self.widget.choices = value
choices = property(_get_choices, _set_choices)
# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label
class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
iterator = ChoiceIterator
def __init__(self, *args, **kwargs):
self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
super(ChoiceField, self).__init__(*args, **kwargs)
class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
iterator = ChoiceIterator
def __init__(self, *args, **kwargs):
self.empty_label = None
super(MultipleChoiceField, self).__init__(*args, **kwargs)
class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField):
iterator = ModelChoiceIterator
def to_python(self, value):
# bypass the queryset value check
if self.null_label is not None and value == self.null_value:
return value
return super(ModelChoiceField, self).to_python(value)
class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField):
iterator = ModelChoiceIterator
def _check_values(self, value):
null = self.null_label is not None and value and self.null_value in value
if null: # remove the null value and any potential duplicates
value = [v for v in value if v != self.null_value]
result = list(super(ModelMultipleChoiceField, self)._check_values(value))
result += [self.null_value] if null else []
return result