Commit 7bda2d8e authored by Marc Tamlyn's avatar Marc Tamlyn
Browse files

Fixed #24837 -- field__contained_by=Range

Provide `contained_by` lookups for the equivalent single valued fields
related to the range field types. This acts as the opposite direction to
rangefield__contains.

With thanks to schinckel for the idea and initial tests.
parent 5987b3c4
Loading
Loading
Loading
Loading
+32 −0
Original line number Diff line number Diff line
@@ -98,6 +98,38 @@ RangeField.register_lookup(lookups.ContainedBy)
RangeField.register_lookup(lookups.Overlap)


class RangeContainedBy(models.Lookup):
    lookup_name = 'contained_by'
    type_mapping = {
        'integer': 'int4range',
        'bigint': 'int8range',
        'double precision': 'numrange',
        'date': 'daterange',
        'timestamp with time zone': 'tstzrange',
    }

    def as_sql(self, qn, connection):
        field = self.lhs.output_field
        if isinstance(field, models.FloatField):
            sql = '%s::numeric <@ %s::{}'.format(self.type_mapping[field.db_type(connection)])
        else:
            sql = '%s <@ %s::{}'.format(self.type_mapping[field.db_type(connection)])
        lhs, lhs_params = self.process_lhs(qn, connection)
        rhs, rhs_params = self.process_rhs(qn, connection)
        params = lhs_params + rhs_params
        return sql % (lhs, rhs), params

    def get_prep_lookup(self):
        return RangeField().get_prep_lookup(self.lookup_name, self.rhs)


models.DateField.register_lookup(RangeContainedBy)
models.DateTimeField.register_lookup(RangeContainedBy)
models.IntegerField.register_lookup(RangeContainedBy)
models.BigIntegerField.register_lookup(RangeContainedBy)
models.FloatField.register_lookup(RangeContainedBy)


@RangeField.register_lookup
class FullyLessThan(lookups.PostgresSimpleLookup):
    lookup_name = 'fully_lt'
+22 −2
Original line number Diff line number Diff line
@@ -631,14 +631,18 @@ model::
    class Event(models.Model):
        name = models.CharField(max_length=200)
        ages = IntegerRangeField()
        start = models.DateTimeField()

        def __str__(self):  # __unicode__ on Python 2
            return self.name

We will also use the following example objects::

    >>> Event.objects.create(name='Soft play', ages=(0, 10))
    >>> Event.objects.create(name='Pub trip', ages=(21, None))
    >>> import datetime
    >>> from django.utils import timezone
    >>> now = timezone.now()
    >>> Event.objects.create(name='Soft play', ages=(0, 10), start=now)
    >>> Event.objects.create(name='Pub trip', ages=(21, None), start=now - datetime.timedelta(days=1))

and ``NumericRange``:

@@ -667,6 +671,22 @@ contained_by
    >>> Event.objects.filter(ages__contained_by=NumericRange(0, 15))
    [<Event: Soft play>]

.. versionadded 1.9

    The `contained_by` lookup is also available on the non-range field types:
    :class:`~django.db.models.fields.IntegerField`,
    :class:`~django.db.models.fields.BigIntegerField`,
    :class:`~django.db.models.fields.FloatField`,
    :class:`~django.db.models.fields.DateField`, and
    :class:`~django.db.models.fields.DateTimeField`. For example::

    >>> from psycopg2.extras import DateTimeTZRange
    >>> Event.objects.filter(start__contained_by=DateTimeTZRange(
    ...     timezone.now() - datetime.timedelta(hours=1),
    ...     timezone.now() + datetime.timedelta(hours=1),
    ... )
    [<Event: Soft play>]

.. fieldlookup:: rangefield.overlap

overlap
+2 −0
Original line number Diff line number Diff line
@@ -91,6 +91,8 @@ Minor features
:mod:`django.contrib.postgres`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* Added support for the :lookup:`rangefield.contained_by` lookup for some built
  in fields which correspond to the range fields.
* Added :class:`~django.contrib.postgres.fields.JSONField`.
* Added :doc:`/ref/contrib/postgres/aggregates`.

+15 −0
Original line number Diff line number Diff line
@@ -143,6 +143,21 @@ class Migration(migrations.Migration):
                ('timestamps', DateTimeRangeField(null=True, blank=True)),
                ('dates', DateRangeField(null=True, blank=True)),
            ],
            options={
                'required_db_vendor': 'postgresql'
            },
            bases=(models.Model,)
        ),
        migrations.CreateModel(
            name='RangeLookupsModel',
            fields=[
                ('parent', models.ForeignKey('postgres_tests.RangesModel', blank=True, null=True)),
                ('integer', models.IntegerField(blank=True, null=True)),
                ('big_integer', models.BigIntegerField(blank=True, null=True)),
                ('float', models.FloatField(blank=True, null=True)),
                ('timestamp', models.DateTimeField(blank=True, null=True)),
                ('date', models.DateField(blank=True, null=True)),
            ],
            options={
                'required_db_vendor': 'postgresql',
            },
+12 −0
Original line number Diff line number Diff line
@@ -60,11 +60,23 @@ if connection.vendor == 'postgresql' and connection.pg_version >= 90200:
        floats = FloatRangeField(blank=True, null=True)
        timestamps = DateTimeRangeField(blank=True, null=True)
        dates = DateRangeField(blank=True, null=True)

    class RangeLookupsModel(PostgreSQLModel):
        parent = models.ForeignKey(RangesModel, blank=True, null=True)
        integer = models.IntegerField(blank=True, null=True)
        big_integer = models.BigIntegerField(blank=True, null=True)
        float = models.FloatField(blank=True, null=True)
        timestamp = models.DateTimeField(blank=True, null=True)
        date = models.DateField(blank=True, null=True)

else:
    # create an object with this name so we don't have failing imports
    class RangesModel(object):
        pass

    class RangeLookupsModel(object):
        pass


# Only create this model for postgres >= 9.4
if connection.vendor == 'postgresql' and connection.pg_version >= 90400:
Loading