diff options
| author | Marc Tamlyn <marc.tamlyn@gmail.com> | 2015-01-10 16:14:20 +0000 |
|---|---|---|
| committer | Marc Tamlyn <marc.tamlyn@gmail.com> | 2015-01-10 16:18:19 +0000 |
| commit | 48ad288679a0cb2e2cfb17f128903e6c5b1c4870 (patch) | |
| tree | 75bacb810dbe071058b5c5cf7d8dcb8e20f3f500 /django | |
| parent | 916e38802f151b34aaca487dc7e928946e81be73 (diff) | |
Fixed #24001 -- Added range fields for PostgreSQL.
Added support for PostgreSQL range types to contrib.postgres.
- 5 new model fields
- 4 new form fields
- New validators
- Uses psycopg2's range type implementation in python
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/postgres/fields/__init__.py | 1 | ||||
| -rw-r--r-- | django/contrib/postgres/fields/ranges.py | 156 | ||||
| -rw-r--r-- | django/contrib/postgres/forms/__init__.py | 1 | ||||
| -rw-r--r-- | django/contrib/postgres/forms/ranges.py | 69 | ||||
| -rw-r--r-- | django/contrib/postgres/validators.py | 15 |
5 files changed, 241 insertions, 1 deletions
diff --git a/django/contrib/postgres/fields/__init__.py b/django/contrib/postgres/fields/__init__.py index 63a0c2c952..bb2685eca1 100644 --- a/django/contrib/postgres/fields/__init__.py +++ b/django/contrib/postgres/fields/__init__.py @@ -1,2 +1,3 @@ from .array import * # NOQA from .hstore import * # NOQA +from .ranges import * # NOQA diff --git a/django/contrib/postgres/fields/ranges.py b/django/contrib/postgres/fields/ranges.py new file mode 100644 index 0000000000..8cb5229593 --- /dev/null +++ b/django/contrib/postgres/fields/ranges.py @@ -0,0 +1,156 @@ +import json + +from django.contrib.postgres import lookups, forms +from django.db import models +from django.utils import six + +from psycopg2.extras import Range, NumericRange, DateRange, DateTimeTZRange + + +__all__ = [ + 'RangeField', 'IntegerRangeField', 'BigIntegerRangeField', + 'FloatRangeField', 'DateTimeRangeField', 'DateRangeField', +] + + +class RangeField(models.Field): + empty_strings_allowed = False + + def get_prep_value(self, value): + if value is None: + return None + elif isinstance(value, Range): + return value + elif isinstance(value, (list, tuple)): + return self.range_type(value[0], value[1]) + return value + + def to_python(self, value): + if isinstance(value, six.string_types): + value = self.range_type(**json.loads(value)) + elif isinstance(value, (list, tuple)): + value = self.range_type(value[0], value[1]) + return value + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + if value is None: + return None + if value.isempty: + return json.dumps({"empty": True}) + return json.dumps({ + "lower": value.lower, + "upper": value.upper, + "bounds": value._bounds, + }) + + def formfield(self, **kwargs): + kwargs.setdefault('form_class', self.form_field) + return super(RangeField, self).formfield(**kwargs) + + +class IntegerRangeField(RangeField): + base_field = models.IntegerField() + range_type = NumericRange + form_field = forms.IntegerRangeField + + def db_type(self, connection): + return 'int4range' + + +class BigIntegerRangeField(RangeField): + base_field = models.BigIntegerField() + range_type = NumericRange + form_field = forms.IntegerRangeField + + def db_type(self, connection): + return 'int8range' + + +class FloatRangeField(RangeField): + base_field = models.FloatField() + range_type = NumericRange + form_field = forms.FloatRangeField + + def db_type(self, connection): + return 'numrange' + + +class DateTimeRangeField(RangeField): + base_field = models.DateTimeField() + range_type = DateTimeTZRange + form_field = forms.DateTimeRangeField + + def db_type(self, connection): + return 'tstzrange' + + +class DateRangeField(RangeField): + base_field = models.DateField() + range_type = DateRange + form_field = forms.DateRangeField + + def db_type(self, connection): + return 'daterange' + + +RangeField.register_lookup(lookups.DataContains) +RangeField.register_lookup(lookups.ContainedBy) +RangeField.register_lookup(lookups.Overlap) + + +@RangeField.register_lookup +class FullyLessThan(lookups.PostgresSimpleLookup): + lookup_name = 'fully_lt' + operator = '<<' + + +@RangeField.register_lookup +class FullGreaterThan(lookups.PostgresSimpleLookup): + lookup_name = 'fully_gt' + operator = '>>' + + +@RangeField.register_lookup +class NotLessThan(lookups.PostgresSimpleLookup): + lookup_name = 'not_lt' + operator = '&>' + + +@RangeField.register_lookup +class NotGreaterThan(lookups.PostgresSimpleLookup): + lookup_name = 'not_gt' + operator = '&<' + + +@RangeField.register_lookup +class AdjacentToLookup(lookups.PostgresSimpleLookup): + lookup_name = 'adjacent_to' + operator = '-|-' + + +@RangeField.register_lookup +class RangeStartsWith(lookups.FunctionTransform): + lookup_name = 'startswith' + function = 'lower' + + @property + def output_field(self): + return self.lhs.output_field.base_field + + +@RangeField.register_lookup +class RangeEndsWith(lookups.FunctionTransform): + lookup_name = 'endswith' + function = 'upper' + + @property + def output_field(self): + return self.lhs.output_field.base_field + + +@RangeField.register_lookup +class IsEmpty(lookups.FunctionTransform): + lookup_name = 'isempty' + function = 'isempty' + output_field = models.BooleanField() diff --git a/django/contrib/postgres/forms/__init__.py b/django/contrib/postgres/forms/__init__.py index 63a0c2c952..bb2685eca1 100644 --- a/django/contrib/postgres/forms/__init__.py +++ b/django/contrib/postgres/forms/__init__.py @@ -1,2 +1,3 @@ from .array import * # NOQA from .hstore import * # NOQA +from .ranges import * # NOQA diff --git a/django/contrib/postgres/forms/ranges.py b/django/contrib/postgres/forms/ranges.py new file mode 100644 index 0000000000..4f8989a96d --- /dev/null +++ b/django/contrib/postgres/forms/ranges.py @@ -0,0 +1,69 @@ +from django.core import exceptions +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from psycopg2.extras import NumericRange, DateRange, DateTimeTZRange + + +__all__ = ['IntegerRangeField', 'FloatRangeField', 'DateTimeRangeField', 'DateRangeField'] + + +class BaseRangeField(forms.MultiValueField): + default_error_messages = { + 'invalid': _('Enter two valid values.'), + 'bound_ordering': _('The start of the range must not exceed the end of the range.'), + } + + def __init__(self, **kwargs): + widget = forms.MultiWidget([self.base_field.widget, self.base_field.widget]) + kwargs.setdefault('widget', widget) + kwargs.setdefault('fields', [self.base_field(required=False), self.base_field(required=False)]) + kwargs.setdefault('required', False) + kwargs.setdefault('require_all_fields', False) + super(BaseRangeField, self).__init__(**kwargs) + + def prepare_value(self, value): + if isinstance(value, self.range_type): + return [value.lower, value.upper] + if value is None: + return [None, None] + return value + + def compress(self, values): + if not values: + return None + lower, upper = values + if lower is not None and upper is not None and lower > upper: + raise exceptions.ValidationError( + self.error_messages['bound_ordering'], + code='bound_ordering', + ) + try: + range_value = self.range_type(lower, upper) + except TypeError: + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + ) + else: + return range_value + + +class IntegerRangeField(BaseRangeField): + base_field = forms.IntegerField + range_type = NumericRange + + +class FloatRangeField(BaseRangeField): + base_field = forms.FloatField + range_type = NumericRange + + +class DateTimeRangeField(BaseRangeField): + base_field = forms.DateTimeField + range_type = DateTimeTZRange + + +class DateRangeField(BaseRangeField): + base_field = forms.DateField + range_type = DateRange diff --git a/django/contrib/postgres/validators.py b/django/contrib/postgres/validators.py index 19d0a69765..49ec921db1 100644 --- a/django/contrib/postgres/validators.py +++ b/django/contrib/postgres/validators.py @@ -1,7 +1,10 @@ import copy from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.core.validators import ( + MaxLengthValidator, MinLengthValidator, MaxValueValidator, + MinValueValidator, +) from django.utils.deconstruct import deconstructible from django.utils.translation import ungettext_lazy, ugettext_lazy as _ @@ -63,3 +66,13 @@ class KeysValidator(object): def __ne__(self, other): return not (self == other) + + +class RangeMaxValueValidator(MaxValueValidator): + compare = lambda self, a, b: a.upper > b + message = _('Ensure that this range is completely less than or equal to %(limit_value)s.') + + +class RangeMinValueValidator(MinValueValidator): + compare = lambda self, a, b: a.lower < b + message = _('Ensure that this range is completely greater than or equal to %(limit_value)s.') |
