Commit 33ea472f authored by Marc Tamlyn's avatar Marc Tamlyn
Browse files

Fixed #24604 -- Added JSONField to contrib.postgres.

parent 74fe4428
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
from .array import *  # NOQA
from .hstore import *  # NOQA
from .jsonb import *  # NOQA
from .ranges import *  # NOQA
+99 −0
Original line number Diff line number Diff line
import json

from psycopg2.extras import Json

from django.contrib.postgres import forms, lookups
from django.core import exceptions
from django.db.models import Field, Transform
from django.utils.translation import ugettext_lazy as _

__all__ = ['JSONField']


class JSONField(Field):
    empty_strings_allowed = False
    description = _('A JSON object')
    default_error_messages = {
        'invalid': _("Value must be valid JSON."),
    }

    def db_type(self, connection):
        return 'jsonb'

    def get_transform(self, name):
        transform = super(JSONField, self).get_transform(name)
        if transform:
            return transform
        return KeyTransformFactory(name)

    def get_prep_value(self, value):
        if value is not None:
            return Json(value)
        return value

    def get_prep_lookup(self, lookup_type, value):
        if lookup_type in ('has_key', 'has_keys', 'has_any_keys'):
            return value
        if isinstance(value, (dict, list)):
            return Json(value)
        return super(JSONField, self).get_prep_lookup(lookup_type, value)

    def validate(self, value, model_instance):
        super(JSONField, self).validate(value, model_instance)
        try:
            json.dumps(value)
        except TypeError:
            raise exceptions.ValidationError(
                self.error_messages['invalid'],
                code='invalid',
                params={'value': value},
            )

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return value

    def formfield(self, **kwargs):
        defaults = {'form_class': forms.JSONField}
        defaults.update(kwargs)
        return super(JSONField, self).formfield(**defaults)


JSONField.register_lookup(lookups.DataContains)
JSONField.register_lookup(lookups.ContainedBy)
JSONField.register_lookup(lookups.HasKey)
JSONField.register_lookup(lookups.HasKeys)
JSONField.register_lookup(lookups.HasAnyKeys)


class KeyTransform(Transform):

    def __init__(self, key_name, *args, **kwargs):
        super(KeyTransform, self).__init__(*args, **kwargs)
        self.key_name = key_name

    def as_sql(self, compiler, connection):
        key_transforms = [self.key_name]
        previous = self.lhs
        while isinstance(previous, KeyTransform):
            key_transforms.insert(0, previous.key_name)
            previous = previous.lhs
        lhs, params = compiler.compile(previous)
        if len(key_transforms) > 1:
            return "{} #> %s".format(lhs), [key_transforms] + params
        try:
            int(self.key_name)
        except ValueError:
            lookup = "'%s'" % self.key_name
        else:
            lookup = "%s" % self.key_name
        return "%s -> %s" % (lhs, lookup), params


class KeyTransformFactory(object):

    def __init__(self, key_name):
        self.key_name = key_name

    def __call__(self, *args, **kwargs):
        return KeyTransform(self.key_name, *args, **kwargs)
+1 −0
Original line number Diff line number Diff line
from .array import *  # NOQA
from .hstore import *  # NOQA
from .jsonb import *  # NOQA
from .ranges import *  # NOQA
+31 −0
Original line number Diff line number Diff line
import json

from django import forms
from django.utils.translation import ugettext_lazy as _

__all__ = ['JSONField']


class JSONField(forms.CharField):
    default_error_messages = {
        'invalid': _("'%(value)s' value must be valid JSON."),
    }

    def __init__(self, **kwargs):
        kwargs.setdefault('widget', forms.Textarea)
        super(JSONField, self).__init__(**kwargs)

    def to_python(self, value):
        if value in self.empty_values:
            return None
        try:
            return json.loads(value)
        except ValueError:
            raise forms.ValidationError(
                self.error_messages['invalid'],
                code='invalid',
                params={'value': value},
            )

    def prepare_value(self, value):
        return json.dumps(value)
+105 −0
Original line number Diff line number Diff line
@@ -450,6 +450,111 @@ using in conjunction with lookups on
    >>> Dog.objects.filter(data__values__contains=['collie'])
    [<Dog: Meg>]

JSONField
---------

.. versionadded:: 1.9

.. class:: JSONField(**options)

    A field for storing JSON encoded data. In Python the data is represented in
    its Python native format: dictionaries, lists, strings, numbers, booleans
    and ``None``.

.. note::

    PostgreSQL has two native JSON based data types: ``json`` and ``jsonb``.
    The main difference between them is how they are stored and how they can be
    queried. PostgreSQL's ``json`` field is stored as the original string
    representation of the JSON and must be decoded on the fly when queried
    based on keys. The ``jsonb`` field is stored based on the actual structure
    of the JSON which allows indexing. The trade-off is a small additional cost
    on writing to the ``jsonb`` field. ``JSONField`` uses ``jsonb``.

    **As a result, the usage of this field is only supported on PostgreSQL
    versions at least 9.4**.

Querying JSONField
^^^^^^^^^^^^^^^^^^

We will use the following example model::

    from django.contrib.postgres.fields import JSONField
    from django.db import models

    class Dog(models.Model):
        name = models.CharField(max_length=200)
        data = JSONField()

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

.. fieldlookup:: jsonfield.key

Key, index, and path lookups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To query based on a given dictionary key, simply use that key as the lookup
name::

    >>> Dog.objects.create(name='Rufus', data={
    ...     'breed': 'labrador',
    ...     'owner': {
    ...         'name': 'Bob',
    ...         'other_pets': [{
    ...             'name': 'Fishy',
    ...         }],
    ...     },
    ... })
    >>> Dog.objects.create(name='Meg', data={'breed': 'collie'})

    >>> Dog.objects.filter(data__breed='collie')
    [<Dog: Meg>]

Multiple keys can be chained together to form a path lookup::

    >>> Dog.objects.filter(data__owner__name='Bob')
    [<Dog: Rufus>]

If the key is an integer, it will be interpreted as an index lookup in an
array::

    >>> Dog.objects.filter(data__owner__other_pets__0__name='Fishy')
    [<Dog: Rufus>]

If the key you wish to query by clashes with the name of another lookup, use
the :lookup:`jsonfield.contains` lookup instead.

If only one key or index is used, the SQL operator ``->`` is used. If multiple
operators are used then the ``#>`` operator is used.

.. warning::

    Since any string could be a key in a JSON object, any lookup other than
    those listed below will be interpreted as a key lookup. No errors are
    raised. Be extra careful for typing mistakes, and always check your queries
    work as you intend.

Containment and key operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. fieldlookup:: jsonfield.contains
.. fieldlookup:: jsonfield.contained_by
.. fieldlookup:: jsonfield.has_key
.. fieldlookup:: jsonfield.has_any_keys
.. fieldlookup:: jsonfield.has_keys

:class:`~django.contrib.postgres.fields.JSONField` shares lookups relating to
containment and keys with :class:`~django.contrib.postgres.fields.HStoreField`.

- :lookup:`contains <hstorefield.contains>` (accepts any JSON rather than
  just a dictionary of strings)
- :lookup:`contained_by <hstorefield.contained_by>` (accepts any JSON
  rather than just a dictionary of strings)
- :lookup:`has_key <hstorefield.has_key>`
- :lookup:`has_any_keys <hstorefield.has_any_keys>`
- :lookup:`has_keys <hstorefield.has_keys>`

.. _range-fields:

Range Fields
Loading