Skip to content

Commit 3fb8866

Browse files
authored
chore: Vendor JSONField to fix runtime warnings and reduce future upgrade pain (#13397)
The packaged version of this library contains metaclass warnings in Django1.8. We could upgrade but newer versions of the library are unwilling to serialize datetimes and decimal objects. Vendoring the library will allow us to maintain compatibility for our use cases and upgrade django at the same time. We can't yet remove the jsonfield dependency as getsentry is using it. Capture the intended behavior from the point we forked off. Refs SEN-686
1 parent 81df3fd commit 3fb8866

20 files changed

+407
-30
lines changed

src/sentry/db/models/fields/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .citext import * # NOQA
1414
from .encrypted import * # NOQA
1515
from .foreignkey import * # NOQA
16+
from .jsonfield import * # NOQA
1617
from .gzippeddict import * # NOQA
1718
from .node import * # NOQA
1819
from .pickle import * # NOQA

src/sentry/db/models/fields/encrypted.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
from django.conf import settings
1010
from django.db.models import CharField, TextField
11-
from jsonfield import JSONField
1211
from picklefield.fields import PickledObjectField
12+
from sentry.db.models.fields.jsonfield import JSONField
1313
from sentry.db.models.utils import Creator
1414
from sentry.utils.encryption import decrypt, encrypt
1515

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from __future__ import absolute_import, unicode_literals
2+
import json
3+
import datetime
4+
import six
5+
6+
from decimal import Decimal
7+
8+
from django.core.exceptions import ValidationError
9+
from django.conf import settings
10+
from django.db import models
11+
from django.utils.translation import ugettext_lazy as _
12+
13+
from sentry.db.models.utils import Creator
14+
15+
16+
def default(o):
17+
if hasattr(o, 'to_json'):
18+
return o.to_json()
19+
if isinstance(o, Decimal):
20+
return six.text_type(o)
21+
if isinstance(o, datetime.datetime):
22+
if o.tzinfo:
23+
return o.strftime('%Y-%m-%dT%H:%M:%S%z')
24+
return o.strftime("%Y-%m-%dT%H:%M:%S")
25+
if isinstance(o, datetime.date):
26+
return o.strftime("%Y-%m-%d")
27+
if isinstance(o, datetime.time):
28+
if o.tzinfo:
29+
return o.strftime('%H:%M:%S%z')
30+
return o.strftime("%H:%M:%S")
31+
32+
raise TypeError(repr(o) + " is not JSON serializable")
33+
34+
35+
class JSONField(models.TextField):
36+
"""
37+
A field that will ensure the data entered into it is valid JSON.
38+
39+
Originally from https://github.com/adamchainz/django-jsonfield/blob/0.9.13/jsonfield/fields.py
40+
Adapted to fit our requirements of:
41+
42+
- always using a text field
43+
- being able to serialize dates/decimals
44+
- not emitting deprecation warnings
45+
"""
46+
default_error_messages = {
47+
'invalid': _("'%s' is not a valid JSON string.")
48+
}
49+
description = "JSON object"
50+
51+
def __init__(self, *args, **kwargs):
52+
if not kwargs.get('null', False):
53+
kwargs['default'] = kwargs.get('default', dict)
54+
self.encoder_kwargs = {
55+
'indent': kwargs.pop('indent', getattr(settings, 'JSONFIELD_INDENT', None))
56+
}
57+
super(JSONField, self).__init__(*args, **kwargs)
58+
self.validate(self.get_default(), None)
59+
60+
def contribute_to_class(self, cls, name):
61+
"""
62+
Add a descriptor for backwards compatibility
63+
with previous Django behavior.
64+
"""
65+
super(JSONField, self).contribute_to_class(cls, name)
66+
setattr(cls, name, Creator(self))
67+
68+
def validate(self, value, model_instance):
69+
if not self.null and value is None:
70+
raise ValidationError(self.error_messages['null'])
71+
try:
72+
self.get_prep_value(value)
73+
except BaseException:
74+
raise ValidationError(self.error_messages['invalid'] % value)
75+
76+
def get_default(self):
77+
if self.has_default():
78+
default = self.default
79+
if callable(default):
80+
default = default()
81+
if isinstance(default, six.string_types):
82+
return json.loads(default)
83+
return json.loads(json.dumps(default))
84+
return super(JSONField, self).get_default()
85+
86+
def get_internal_type(self):
87+
return 'TextField'
88+
89+
def db_type(self, connection):
90+
return 'text'
91+
92+
def to_python(self, value):
93+
if isinstance(value, six.string_types):
94+
if value == "":
95+
if self.null:
96+
return None
97+
if self.blank:
98+
return ""
99+
try:
100+
value = json.loads(value)
101+
except ValueError:
102+
msg = self.error_messages['invalid'] % value
103+
raise ValidationError(msg)
104+
# TODO: Look for date/time/datetime objects within the structure?
105+
return value
106+
107+
def get_db_prep_value(self, value, connection=None, prepared=None):
108+
return self.get_prep_value(value)
109+
110+
def get_prep_value(self, value):
111+
if value is None:
112+
if not self.null and self.blank:
113+
return ""
114+
return None
115+
return json.dumps(value, default=default, **self.encoder_kwargs)
116+
117+
def get_prep_lookup(self, lookup_type, value):
118+
if lookup_type in ["exact", "iexact"]:
119+
return self.to_python(self.get_prep_value(value))
120+
if lookup_type == "in":
121+
return [self.to_python(self.get_prep_value(v)) for v in value]
122+
if lookup_type == "isnull":
123+
return value
124+
if lookup_type in ["contains", "icontains"]:
125+
if isinstance(value, (list, tuple)):
126+
raise TypeError("Lookup type %r not supported with argument of %s" % (
127+
lookup_type, type(value).__name__
128+
))
129+
# Need a way co combine the values with '%', but don't escape that.
130+
return self.get_prep_value(value)[1:-1].replace(', ', r'%')
131+
if isinstance(value, dict):
132+
return self.get_prep_value(value)[1:-1]
133+
return self.to_python(self.get_prep_value(value))
134+
raise TypeError('Lookup type %r not supported' % lookup_type)
135+
136+
def value_to_string(self, obj):
137+
return self._get_val_from_obj(obj)
138+
139+
140+
if 'south' in settings.INSTALLED_APPS:
141+
from south.modelsinspector import add_introspection_rules
142+
add_introspection_rules([], ['^sentry\.db\.models\.fields\.JSONField'])

src/sentry/models/discoversavedquery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import absolute_import
22
from django.db import models, transaction
3-
from jsonfield import JSONField
3+
from sentry.db.models.fields import JSONField
44
from sentry.db.models import (
55
Model, FlexibleForeignKey, sane_repr
66
)

src/sentry/models/externalissue.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
from django.db import models
44
from django.utils import timezone
5-
from jsonfield import JSONField
65

7-
from sentry.db.models import BoundedPositiveIntegerField, Model, sane_repr
6+
from sentry.db.models import (
7+
BoundedPositiveIntegerField,
8+
JSONField,
9+
Model,
10+
sane_repr
11+
)
812

913

1014
class ExternalIssue(Model):

src/sentry/models/featureadoption.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
from django.db import models, IntegrityError, transaction
66
from django.utils import timezone
7-
from jsonfield import JSONField
87

98
from sentry.adoption import manager
109
from sentry.adoption.manager import UnknownFeature
11-
from sentry.db.models import (BaseManager, FlexibleForeignKey, Model, sane_repr)
10+
from sentry.db.models import (
11+
BaseManager,
12+
FlexibleForeignKey,
13+
JSONField,
14+
Model,
15+
sane_repr
16+
)
1217
from sentry.utils import redis
1318

1419
logger = logging.getLogger(__name__)

src/sentry/models/file.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@
2525
from django.core.files.storage import get_storage_class
2626
from django.db import models, transaction, IntegrityError
2727
from django.utils import timezone
28-
from jsonfield import JSONField
2928

3029
from sentry.app import locks
31-
from sentry.db.models import (BoundedPositiveIntegerField, FlexibleForeignKey, Model)
30+
from sentry.db.models import (
31+
BoundedPositiveIntegerField,
32+
FlexibleForeignKey,
33+
JSONField,
34+
Model
35+
)
3236
from sentry.tasks.files import delete_file as delete_file_task
3337
from sentry.utils import metrics
3438
from sentry.utils.retries import TimedRetryPolicy

src/sentry/models/grouplink.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
from django.db import models
1111
from django.utils import timezone
1212
from django.utils.translation import ugettext_lazy as _
13-
from jsonfield import JSONField
1413

15-
from sentry.db.models import Model, sane_repr, BoundedBigIntegerField, BoundedPositiveIntegerField
14+
from sentry.db.models import (
15+
Model,
16+
sane_repr,
17+
BoundedBigIntegerField,
18+
BoundedPositiveIntegerField,
19+
JSONField,
20+
)
1621

1722

1823
class GroupLink(Model):

src/sentry/models/groupsnooze.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
from django.db import models
66
from django.utils import timezone
7-
from jsonfield import JSONField
87

98
from sentry.db.models import (
10-
BaseManager, BoundedPositiveIntegerField, FlexibleForeignKey, Model, sane_repr
9+
BaseManager,
10+
BoundedPositiveIntegerField,
11+
FlexibleForeignKey,
12+
JSONField,
13+
Model,
14+
sane_repr
1115
)
1216

1317

src/sentry/models/organizationonboardingtask.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from django.core.cache import cache
1212
from django.db import models, IntegrityError, transaction
1313
from django.utils import timezone
14-
from jsonfield import JSONField
1514

1615
from sentry.db.models import (
17-
BaseManager, BoundedBigIntegerField, BoundedPositiveIntegerField, FlexibleForeignKey, Model,
16+
BaseManager, BoundedBigIntegerField, BoundedPositiveIntegerField,
17+
FlexibleForeignKey, JSONField, Model,
1818
sane_repr
1919
)
2020

src/sentry/models/projectkey.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from bitfield import BitField
1515
from uuid import uuid4
1616

17-
from jsonfield import JSONField
1817
from django.conf import settings
1918
from django.core.urlresolvers import reverse
2019
from django.db import models
@@ -24,7 +23,12 @@
2423

2524
from sentry import options
2625
from sentry.db.models import (
27-
Model, BaseManager, BoundedPositiveIntegerField, FlexibleForeignKey, sane_repr
26+
Model,
27+
BaseManager,
28+
BoundedPositiveIntegerField,
29+
FlexibleForeignKey,
30+
JSONField,
31+
sane_repr
2832
)
2933

3034
_uuid4_re = re.compile(r'^[a-f0-9]{32}$')

src/sentry/models/projectownership.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
import operator
44

5-
from jsonfield import JSONField
65

76
from django.db import models
87
from django.db.models import Q
98
from django.utils import timezone
109

1110
from sentry.db.models import Model, sane_repr
12-
from sentry.db.models.fields import FlexibleForeignKey
11+
from sentry.db.models.fields import FlexibleForeignKey, JSONField
1312
from sentry.ownership.grammar import load_schema
1413
from functools import reduce
1514

src/sentry/models/prompts_activity.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
from django.db import models
44
from django.conf import settings
55
from django.utils import timezone
6-
from jsonfield import JSONField
76

8-
from sentry.db.models import (BoundedPositiveIntegerField, FlexibleForeignKey, Model, sane_repr)
7+
from sentry.db.models import (
8+
BoundedPositiveIntegerField,
9+
FlexibleForeignKey,
10+
JSONField,
11+
Model,
12+
sane_repr
13+
)
914

1015

1116
class PromptsActivity(Model):

src/sentry/models/release.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from django.db import models, IntegrityError, transaction
1616
from django.db.models import F
1717
from django.utils import timezone
18-
from jsonfield import JSONField
1918
from time import time
2019

2120
from sentry.app import locks
2221
from sentry.db.models import (
23-
ArrayField, BoundedPositiveIntegerField, FlexibleForeignKey, Model, sane_repr
22+
ArrayField, BoundedPositiveIntegerField, FlexibleForeignKey,
23+
JSONField, Model, sane_repr
2424
)
2525

2626
from sentry.models import CommitFileChange

src/sentry/models/repository.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
from django.db import models
44
from django.db.models.signals import pre_delete
55
from django.utils import timezone
6-
from jsonfield import JSONField
76

87
from sentry.constants import ObjectStatus
9-
from sentry.db.models import (BoundedPositiveIntegerField, Model, sane_repr)
8+
from sentry.db.models import (
9+
BoundedPositiveIntegerField,
10+
JSONField,
11+
Model,
12+
sane_repr
13+
)
1014
from sentry.db.mixin import PendingDeletionMixin, delete_pending_deletion_option
1115
from sentry.signals import pending_delete
1216

src/sentry/models/scheduledeletion.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
from django.db import models
55
from django.db.models import get_model
66
from django.utils import timezone
7-
from jsonfield import JSONField
87
from uuid import uuid4
98

10-
from sentry.db.models import BoundedBigIntegerField, Model
9+
from sentry.db.models import (
10+
BoundedBigIntegerField,
11+
JSONField,
12+
Model
13+
)
1114

1215

1316
def default_guid():

src/sentry/models/scheduledjob.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from django.db import models
44
from django.utils import timezone
5-
from jsonfield import JSONField
65

7-
from sentry.db.models import (Model, sane_repr)
6+
from sentry.db.models import (JSONField, Model, sane_repr)
87

98

109
def schedule_jobs(jobs):

src/sentry/models/widget.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
from django.db import models
44
from django.utils import timezone
5-
from jsonfield import JSONField
65

76
from sentry.constants import ObjectStatus
8-
from sentry.db.models import BoundedPositiveIntegerField, FlexibleForeignKey, Model, sane_repr
7+
from sentry.db.models import (
8+
BoundedPositiveIntegerField,
9+
FlexibleForeignKey,
10+
JSONField,
11+
Model,
12+
sane_repr
13+
)
914

1015

1116
class TypesClass(object):

0 commit comments

Comments
 (0)