diff --git a/AUTHORS b/AUTHORS index 3c39a273f2..ca89ff4a76 100644 --- a/AUTHORS +++ b/AUTHORS @@ -263,6 +263,7 @@ answer newbie questions, and generally made Django that much better: Filip Noetzel Filip Wasilewski Finn Gruwier Larsen + Flávio Juvenal da Silva Junior flavio.curella@gmail.com Florian Apolloner Francisco Albarran Cristobal diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py index 7992740663..5c9bb1b2ee 100644 --- a/django/contrib/postgres/fields/array.py +++ b/django/contrib/postgres/fields/array.py @@ -10,17 +10,19 @@ from django.utils.inspect import func_supports_parameter from django.utils.translation import gettext_lazy as _ from ..utils import prefix_validation_error +from .mixins import CheckFieldDefaultMixin from .utils import AttributeSetter __all__ = ['ArrayField'] -class ArrayField(Field): +class ArrayField(CheckFieldDefaultMixin, Field): empty_strings_allowed = False default_error_messages = { 'item_invalid': _('Item %(nth)s in the array did not validate: '), 'nested_array_mismatch': _('Nested arrays must have the same length.'), } + _default_hint = ('list', '[]') def __init__(self, base_field, size=None, **kwargs): self.base_field = base_field diff --git a/django/contrib/postgres/fields/jsonb.py b/django/contrib/postgres/fields/jsonb.py index a06187c4bc..4ed441003b 100644 --- a/django/contrib/postgres/fields/jsonb.py +++ b/django/contrib/postgres/fields/jsonb.py @@ -9,6 +9,8 @@ from django.db.models import ( ) from django.utils.translation import gettext_lazy as _ +from .mixins import CheckFieldDefaultMixin + __all__ = ['JSONField'] @@ -25,12 +27,13 @@ class JsonAdapter(Json): return json.dumps(obj, **options) -class JSONField(Field): +class JSONField(CheckFieldDefaultMixin, Field): empty_strings_allowed = False description = _('A JSON object') default_error_messages = { 'invalid': _("Value must be valid JSON."), } + _default_hint = ('dict', '{}') def __init__(self, verbose_name=None, name=None, encoder=None, **kwargs): if encoder and not callable(encoder): diff --git a/django/contrib/postgres/fields/mixins.py b/django/contrib/postgres/fields/mixins.py new file mode 100644 index 0000000000..254b80c4a2 --- /dev/null +++ b/django/contrib/postgres/fields/mixins.py @@ -0,0 +1,29 @@ +from django.core import checks + + +class CheckFieldDefaultMixin: + _default_hint = ('', '') + + def _check_default(self): + if self.has_default() and self.default is not None and not callable(self.default): + return [ + checks.Warning( + "%s default should be a callable instead of an instance so " + "that it's not shared between all field instances." % ( + self.__class__.__name__, + ), + hint=( + 'Use a callable instead, e.g., use `%s` instead of ' + '`%s`.' % self._default_hint + ), + obj=self, + id='postgres.E003', + ) + ] + else: + return [] + + def check(self, **kwargs): + errors = super().check(**kwargs) + errors.extend(self._check_default()) + return errors diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 05d2b55d34..dc70022354 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -687,6 +687,8 @@ fields: * **postgres.E001**: Base field for array has errors: ... * **postgres.E002**: Base field for array cannot be a related field. +* **postgres.E003**: ```` default should be a callable instead of an + instance so that it's not shared between all field instances. ``sites`` --------- diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index fa390daaa8..35e76d0622 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -41,7 +41,7 @@ class PostgreSQLModel(models.Model): class IntegerArrayModel(PostgreSQLModel): - field = ArrayField(models.IntegerField(), default=[], blank=True) + field = ArrayField(models.IntegerField(), default=list, blank=True) class NullableIntegerArrayModel(PostgreSQLModel): diff --git a/tests/postgres_tests/test_array.py b/tests/postgres_tests/test_array.py index e2e4ccdeb2..77ac049ce4 100644 --- a/tests/postgres_tests/test_array.py +++ b/tests/postgres_tests/test_array.py @@ -4,7 +4,7 @@ import unittest import uuid from django import forms -from django.core import exceptions, serializers, validators +from django.core import checks, exceptions, serializers, validators from django.core.exceptions import FieldError from django.core.management import call_command from django.db import IntegrityError, connection, models @@ -424,6 +424,38 @@ class TestChecks(PostgreSQLTestCase): self.assertEqual(len(errors), 1) self.assertEqual(errors[0].id, 'postgres.E002') + def test_invalid_default(self): + class MyModel(PostgreSQLModel): + field = ArrayField(models.IntegerField(), default=[]) + + model = MyModel() + self.assertEqual(model.check(), [ + checks.Warning( + msg=( + "ArrayField default should be a callable instead of an " + "instance so that it's not shared between all field " + "instances." + ), + hint='Use a callable instead, e.g., use `list` instead of `[]`.', + obj=MyModel._meta.get_field('field'), + id='postgres.E003', + ) + ]) + + def test_valid_default(self): + class MyModel(PostgreSQLModel): + field = ArrayField(models.IntegerField(), default=list) + + model = MyModel() + self.assertEqual(model.check(), []) + + def test_valid_default_none(self): + class MyModel(PostgreSQLModel): + field = ArrayField(models.IntegerField(), default=None) + + model = MyModel() + self.assertEqual(model.check(), []) + def test_nested_field_checks(self): """ Nested ArrayFields are permitted. diff --git a/tests/postgres_tests/test_json.py b/tests/postgres_tests/test_json.py index cdabca04d6..acbd855f1a 100644 --- a/tests/postgres_tests/test_json.py +++ b/tests/postgres_tests/test_json.py @@ -2,13 +2,14 @@ import datetime import uuid from decimal import Decimal -from django.core import exceptions, serializers +from django.core import checks, exceptions, serializers from django.core.serializers.json import DjangoJSONEncoder from django.forms import CharField, Form, widgets +from django.test.utils import isolate_apps from django.utils.html import escape from . import PostgreSQLTestCase -from .models import JSONModel +from .models import JSONModel, PostgreSQLModel try: from django.contrib.postgres import forms @@ -259,6 +260,42 @@ class TestQuerying(PostgreSQLTestCase): self.assertTrue(JSONModel.objects.filter(field__foo__iregex=r'^bAr$').exists()) +@isolate_apps('postgres_tests') +class TestChecks(PostgreSQLTestCase): + + def test_invalid_default(self): + class MyModel(PostgreSQLModel): + field = JSONField(default={}) + + model = MyModel() + self.assertEqual(model.check(), [ + checks.Warning( + msg=( + "JSONField default should be a callable instead of an " + "instance so that it's not shared between all field " + "instances." + ), + hint='Use a callable instead, e.g., use `dict` instead of `{}`.', + obj=MyModel._meta.get_field('field'), + id='postgres.E003', + ) + ]) + + def test_valid_default(self): + class MyModel(PostgreSQLModel): + field = JSONField(default=dict) + + model = MyModel() + self.assertEqual(model.check(), []) + + def test_valid_default_none(self): + class MyModel(PostgreSQLModel): + field = JSONField(default=None) + + model = MyModel() + self.assertEqual(model.check(), []) + + class TestSerialization(PostgreSQLTestCase): test_data = ( '[{"fields": {"field": {"a": "b", "c": null}, "field_custom": null}, '