Skip to content

Commit 1ad0cd3

Browse files
Init
0 parents  commit 1ad0cd3

23 files changed

+549
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*~
2+
*.pyc
3+
*.db

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Derek Schaefer

LICENSE

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright (c) 2012, Derek Schaefer.
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are met:
6+
7+
1) Redistributions of source code must retain the above copyright notice,
8+
this list of conditions and the following disclaimer.
9+
2) Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in the
11+
documentation and/or other materials provided with the distribution.
12+
3) Neither the name of django-json-field nor the names of its contributors
13+
may be used to endorse or promote products derived from this software
14+
without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

MANIFEST

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# file GENERATED by distutils, do NOT edit
2+
LICENSE
3+
setup.py
4+
json_field/__init__.py
5+
json_field/fields.py
6+
json_field/forms.py
7+
json_field/models.py
8+
json_field/tests.py
9+
json_field/views.py

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include LICENSE

README.rst

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
Django JSON Field
2+
=================
3+
4+
``django-json-field`` contains a flexible JSONField and associated form field. The model field is not only capable of serializing common JSON data types (int, float, decimal, string, time, date, datetime, etc.) but also lazily deserializing them so they can be accessed and modified as normal Python objects within Django.
5+
6+
A form field is also provided. It will accept serialized representations (i.e. ``{"date": "2012-04-23T19:16:54.133", "num": "1.2399999999999999911182158029987476766109466552734375"}``) but also provides safe access to the ``datetime`` module and ``Decimal`` class for explicit use (i.e. ``{"date": datetime.datetime(2012, 4, 23, 19, 16, 54, 133000), "num": Decimal('1.2399999999999999911182158029987476766109466552734375')}``).
7+
8+
While the JSON string will not be deserialized until it is accessed it can still be a performance concern. You may find it valuable to disable deserialization (``JSONField(decoder=None)``), or to defer loading the field altogether (i.e. ``MyModel.objects.all().defer('json')``).
9+
10+
``django-json-field`` is also compatible with South.
11+
12+
Installation
13+
------------
14+
15+
Install from PyPI:
16+
17+
``pip install django-json-field``
18+
19+
Install from GitHub:
20+
21+
``git clone git://github.com/derek-schaefer/django-json-field.git``
22+
23+
``pip install -e git+git://github.com/derek-schaefer/django-json-field.git#egg=json_field``
24+
25+
Configuration
26+
-------------
27+
28+
Add ``json_field`` to your ``PYTHONPATH`` and ``INSTALLED_APPS`` setting:
29+
30+
::
31+
32+
INSTALLED_APPS = (
33+
...
34+
'json_field',
35+
...
36+
)
37+
38+
That's all!
39+
40+
Usage
41+
-----
42+
43+
Add a ``JSONField`` to your model like any other field.
44+
45+
::
46+
47+
from json_field import JSONField
48+
from django.db import models
49+
50+
class MyModel(models.Model):
51+
52+
json = JSONField()
53+
54+
``JSONField`` also has a few additional optional parameters.
55+
56+
- ``default``: Falls back on ``{}`` if not provided.
57+
- ``db_type``: Allows you to specify the column type (default: ``text``)
58+
- ``encoder``: Custom JSON encoder (default: ``DjangoJSONEncoder``)
59+
- ``decoder``: Custom JSON decoder (default: ``json_fields.fields.JSONDecoder``)
60+
- ``encoder_kwargs``: Specify all arguments to the encoder (overrides ``encoder``)
61+
- ``decoder_kwargs``: Specify all arguments to the decoder (overrides ``decoder``)
62+
63+
License
64+
-------
65+
66+
``django-json-field`` is licensed under the New BSD license.

json_field/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
try:
2+
from json_field.fields import JSONField
3+
except ImportError:
4+
pass # fails when imported by setup.py, no worries
5+
6+
__version__ = '0.1'

json_field/fields.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from json_field.forms import JSONFormField
2+
3+
from django.db import models
4+
from django.utils import simplejson as json
5+
from django.core import exceptions
6+
from django.core.serializers.json import DjangoJSONEncoder
7+
from django.utils.translation import ugettext as _
8+
9+
import decimal
10+
from datetime import datetime
11+
from dateutil import parser as date_parser
12+
13+
class JSONDecoder(json.JSONDecoder):
14+
15+
_recursable_types = [str, unicode, list, dict]
16+
17+
def _is_recursive(self, obj):
18+
return type(obj) in JSONDecoder._recursable_types
19+
20+
def decode(self, obj, *args, **kwargs):
21+
if not kwargs.get('recurse', False):
22+
obj = super(JSONDecoder, self).decode(obj, *args, **kwargs)
23+
if isinstance(obj, list):
24+
for i in xrange(len(obj)):
25+
item = obj[i]
26+
if self._is_recursive(item):
27+
obj[i] = self.decode(item, recurse=True)
28+
elif isinstance(obj, dict):
29+
for key, value in obj.items():
30+
if self._is_recursive(value):
31+
obj[key] = self.decode(value, recurse=True)
32+
elif isinstance(obj, basestring):
33+
try:
34+
return decimal.Decimal(obj) # first since dateutil will consume this
35+
except decimal.InvalidOperation:
36+
pass
37+
try:
38+
return datetime.strptime(obj, '%H:%M:%S.%f').time() # with microsecond
39+
except ValueError:
40+
pass
41+
try:
42+
return datetime.strptime(obj, '%H:%M:%S').time() # without microsecond
43+
except ValueError:
44+
pass
45+
try:
46+
return datetime.strptime(obj, '%Y-%m-%d').date() # simple date
47+
except ValueError:
48+
pass
49+
try:
50+
return date_parser.parse(obj) # supports multiple formats
51+
except ValueError:
52+
pass
53+
return obj
54+
55+
class JSONField(models.TextField):
56+
""" Stores and loads valid JSON objects. """
57+
58+
__metaclass__ = models.SubfieldBase
59+
60+
description = 'JSON object'
61+
62+
default_error_messages = {
63+
'invalid': _(u'Enter a valid JSON object')
64+
}
65+
66+
def __init__(self, *args, **kwargs):
67+
self._db_type = kwargs.pop('db_type', None)
68+
encoder = kwargs.pop('encoder', DjangoJSONEncoder)
69+
decoder = kwargs.pop('decoder', JSONDecoder)
70+
encoder_kwargs = kwargs.pop('encoder_kwargs', {})
71+
decoder_kwargs = kwargs.pop('decoder_kwargs', {})
72+
if not encoder_kwargs and encoder:
73+
encoder_kwargs.update({'cls':encoder})
74+
if not decoder_kwargs and decoder:
75+
decoder_kwargs.update({'cls':decoder})
76+
self.encoder_kwargs = encoder_kwargs
77+
self.decoder_kwargs = decoder_kwargs
78+
kwargs['default'] = kwargs.get('default', {})
79+
kwargs['help_text'] = kwargs.get('help_text', self.default_error_messages['invalid'])
80+
super(JSONField, self).__init__(*args, **kwargs)
81+
82+
def db_type(self, connection):
83+
if self._db_type:
84+
return self._db_type
85+
return super(JSONField, self).db_type(connection)
86+
87+
def to_python(self, value):
88+
if not value:
89+
return None
90+
if isinstance(value, basestring):
91+
try:
92+
value = json.loads(value, **self.decoder_kwargs)
93+
except json.JSONDecodeError:
94+
pass
95+
return value
96+
97+
def get_db_prep_value(self, value, *args, **kwargs):
98+
if value is None:
99+
return None
100+
return json.dumps(value, **self.encoder_kwargs)
101+
102+
def value_to_string(self, obj):
103+
return self.get_db_prep_value(self._get_val_from_obj(obj))
104+
105+
def value_from_object(self, obj):
106+
return json.dumps(super(JSONField, self).value_from_object(obj), **self.encoder_kwargs)
107+
108+
def formfield(self, **kwargs):
109+
defaults = {
110+
'form_class': kwargs.get('form_class', JSONFormField),
111+
'encoder_kwargs': self.encoder_kwargs,
112+
'decoder_kwargs': self.decoder_kwargs,
113+
}
114+
defaults.update(kwargs)
115+
return super(JSONField, self).formfield(**defaults)
116+
117+
try:
118+
# add support for South migrations
119+
from south.modelsinspector import add_introspection_rules
120+
add_introspection_rules([], ['^json_field\.fields\.JSONField'])
121+
except ImportError:
122+
pass

json_field/forms.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.forms import fields, util
2+
from django.utils import simplejson as json
3+
4+
import datetime
5+
import decimal
6+
7+
class JSONFormField(fields.Field):
8+
9+
def __init__(self, *args, **kwargs):
10+
self.encoder_kwargs = kwargs.pop('encoder_kwargs')
11+
self.decoder_kwargs = kwargs.pop('decoder_kwargs')
12+
super(JSONFormField, self).__init__(*args, **kwargs)
13+
14+
def clean(self, value):
15+
# Have to jump through a few hoops to make this reliable
16+
value = super(JSONFormField, self).clean(value)
17+
json_globals = {
18+
'__builtins__': None,
19+
'datetime': datetime,
20+
'Decimal': decimal.Decimal,
21+
}
22+
value = json.dumps(eval(value, json_globals, {}), **self.encoder_kwargs)
23+
try:
24+
json.loads(value, **self.decoder_kwargs)
25+
except ValueError, e:
26+
raise util.ValidationError(self.help_text)
27+
return value

json_field/models.py

Whitespace-only changes.

json_field/tests.py

Whitespace-only changes.

json_field/views.py

Whitespace-only changes.

manage.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python
2+
import os
3+
import sys
4+
5+
if __name__ == "__main__":
6+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
7+
8+
from django.core.management import execute_from_command_line
9+
10+
execute_from_command_line(sys.argv)

setup.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import json_field
2+
3+
from distutils.core import setup
4+
5+
description = 'Generic JSON model and form fields.'
6+
7+
try:
8+
with open('README.rst') as f:
9+
long_description = f.read()
10+
except IOError:
11+
long_description = description
12+
13+
setup(
14+
name = 'django-json-field',
15+
version = json_field.__version__,
16+
description = description,
17+
author = 'Derek Schaefer',
18+
author_email = '[email protected]',
19+
url = 'https://github.com/derek-schaefer/django-json-field',
20+
long_description = long_description,
21+
packages = ['json_field'],
22+
classifiers = [
23+
'Development Status :: 3 - Alpha',
24+
'Intended Audience :: Developers',
25+
'License :: OSI Approved :: BSD License',
26+
'Operating System :: OS Independent',
27+
'Programming Language :: Python',
28+
'Topic :: Software Development :: Libraries :: Python Modules',
29+
],
30+
)

test_project/__init__.py

Whitespace-only changes.

test_project/app/__init__.py

Whitespace-only changes.

test_project/app/admin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from django.contrib import admin
2+
3+
from test_project.app.models import Test
4+
admin.site.register(Test)

test_project/app/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from json_field import JSONField
2+
3+
from django.db import models
4+
5+
class Test(models.Model):
6+
7+
json = JSONField()
8+
json_null = JSONField(blank=True, null=True)

test_project/app/tests.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from test_project.app.models import Test
2+
3+
from django.test import TestCase
4+
from django.db.utils import IntegrityError
5+
6+
from decimal import Decimal
7+
8+
class JSONFieldTest(TestCase):
9+
10+
def test_simple(self):
11+
t1 = Test.objects.create(json=123)
12+
self.assertEqual(123, t1.json)
13+
t2 = Test.objects.create(json='123')
14+
self.assertEqual(123, t2.json)
15+
t3 = Test.objects.create(json=[123])
16+
self.assertEqual([123], t3.json)
17+
t4 = Test.objects.create(json='[123]')
18+
self.assertEqual([123], t4.json)
19+
t5 = Test.objects.create(json={'test':[1,2,3]})
20+
self.assertEqual({'test':[1,2,3]}, t5.json)
21+
t6 = Test.objects.create(json='{"test":[1,2,3]}')
22+
self.assertEqual({'test':[1,2,3]}, t6.json)
23+
24+
def test_null(self):
25+
with self.assertRaises(IntegrityError):
26+
Test.objects.create(json=None)
27+
with self.assertRaises(IntegrityError):
28+
Test.objects.create(json='')
29+
Test.objects.create(json_null=None)
30+
Test.objects.create(json_null='')
31+
32+
def test_decimal(self):
33+
t1 = Test.objects.create(json=Decimal(1.24))
34+
self.assertEqual(Decimal(1.24), t1.json)
35+
t2 = Test.objects.create(json={'test':[{'test':Decimal(1.24)}]})
36+
self.assertEqual({'test':[{'test':Decimal(1.24)}]}, t2.json)
37+
38+
def test_time(self):
39+
pass
40+
41+
def test_date(self):
42+
pass
43+
44+
def test_datetime(self):
45+
pass

test_project/app/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your views here.

0 commit comments

Comments
 (0)