From 1f6b9a3ae1eb6c395034bf24d7a249c73cd74e33 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sun, 23 Mar 2025 15:01:04 -0300 Subject: [PATCH 01/16] Add size parameter --- django_mongodb_backend/fields/array.py | 14 +++++++++++++- django_mongodb_backend/fields/validators.py | 19 +++++++++++++++++++ tests/model_fields_/test_arrayfield.py | 17 +++++++++++++++++ tests/validators_/__init__.py | 0 tests/validators_/tests.py | 14 ++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 django_mongodb_backend/fields/validators.py create mode 100644 tests/validators_/__init__.py create mode 100644 tests/validators_/tests.py diff --git a/django_mongodb_backend/fields/array.py b/django_mongodb_backend/fields/array.py index baf33fc04..e8178f562 100644 --- a/django_mongodb_backend/fields/array.py +++ b/django_mongodb_backend/fields/array.py @@ -10,6 +10,7 @@ from ..forms import SimpleArrayField from ..query_utils import process_lhs, process_rhs from ..utils import prefix_validation_error +from .validators import LengthValidator __all__ = ["ArrayField"] @@ -27,14 +28,23 @@ class ArrayField(CheckFieldDefaultMixin, Field): } _default_hint = ("list", "[]") - def __init__(self, base_field, max_size=None, **kwargs): + def __init__(self, base_field, max_size=None, size=None, **kwargs): self.base_field = base_field self.max_size = max_size + self.size = size + if size and max_size: + raise ValueError("Cannot define both, size and max_size") if self.max_size: self.default_validators = [ *self.default_validators, ArrayMaxLengthValidator(self.max_size), ] + if self.size: + self.default_validators = [ + *self.default_validators, + ArrayMaxLengthValidator(self.size), + LengthValidator(self.size), + ] # For performance, only add a from_db_value() method if the base field # implements it. if hasattr(self.base_field, "from_db_value"): @@ -127,6 +137,8 @@ def deconstruct(self): kwargs["base_field"] = self.base_field.clone() if self.max_size is not None: kwargs["max_size"] = self.max_size + if self.size is not None: + kwargs["size"] = self.size return name, path, args, kwargs def to_python(self, value): diff --git a/django_mongodb_backend/fields/validators.py b/django_mongodb_backend/fields/validators.py new file mode 100644 index 000000000..1c6eb21bc --- /dev/null +++ b/django_mongodb_backend/fields/validators.py @@ -0,0 +1,19 @@ +from django.core.validators import BaseValidator +from django.utils.deconstruct import deconstructible +from django.utils.translation import ngettext_lazy + + +@deconstructible +class LengthValidator(BaseValidator): + message = ngettext_lazy( + "List contains %(show_value)d item, it should contain %(limit_value)d.", + "List contains %(show_value)d items, it should contain %(limit_value)d.", + "show_value", + ) + code = "length" + + def compare(self, a, b): + return a != b + + def clean(self, x): + return len(x) diff --git a/tests/model_fields_/test_arrayfield.py b/tests/model_fields_/test_arrayfield.py index e1537b970..3149a5182 100644 --- a/tests/model_fields_/test_arrayfield.py +++ b/tests/model_fields_/test_arrayfield.py @@ -818,6 +818,23 @@ def test_with_max_size_singular(self): with self.assertRaisesMessage(exceptions.ValidationError, msg): field.clean([1, 2], None) + def test_with_size(self): + field = ArrayField(models.IntegerField(), size=3) + field.clean([1, 2, 3], None) + with self.assertRaises(exceptions.ValidationError) as cm: + field.clean([1, 2, 3, 4], None) + self.assertEqual( + cm.exception.messages[0], + "List contains 4 items, it should contain 3.", + ) + + def test_with_size_singular(self): + field = ArrayField(models.IntegerField(), size=2) + field.clean([1, 2], None) + msg = "List contains 1 item, it should contain 2." + with self.assertRaisesMessage(exceptions.ValidationError, msg): + field.clean([1], None) + def test_nested_array_mismatch(self): field = ArrayField(ArrayField(models.IntegerField())) field.clean([[1, 2], [3, 4]], None) diff --git a/tests/validators_/__init__.py b/tests/validators_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/validators_/tests.py b/tests/validators_/tests.py new file mode 100644 index 000000000..2eb690abf --- /dev/null +++ b/tests/validators_/tests.py @@ -0,0 +1,14 @@ +from django.core.exceptions import ValidationError +from django.test import SimpleTestCase + +from django_mongodb_backend.fields.validators import LengthValidator + + +class TestValidators(SimpleTestCase): + def test_validators(self): + validator = LengthValidator(10) + with self.assertRaises(ValidationError): + validator([]) + with self.assertRaises(ValidationError): + validator(list(range(11))) + self.assertEqual(validator(list(range(10))), None) From 93152012a0a9f67cd4bf12ea93dcbcb9027bf807 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sun, 23 Mar 2025 15:53:42 -0300 Subject: [PATCH 02/16] Docs --- docs/source/ref/models/fields.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/source/ref/models/fields.rst b/docs/source/ref/models/fields.rst index fa0672dc1..7941cca2b 100644 --- a/docs/source/ref/models/fields.rst +++ b/docs/source/ref/models/fields.rst @@ -8,12 +8,13 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. ``ArrayField`` -------------- -.. class:: ArrayField(base_field, max_size=None, **options) +.. class:: ArrayField(base_field, max_size=None, size=None, **options) A field for storing lists of data. Most field types can be used, and you pass another field instance as the :attr:`base_field `. You may also specify a :attr:`max_size - `. ``ArrayField`` can be nested to store + ` and :attr:`size + `. ``ArrayField`` can be nested to store multi-dimensional arrays. If you give the field a :attr:`~django.db.models.Field.default`, ensure @@ -50,9 +51,13 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. board = ArrayField( ArrayField( models.CharField(max_length=10, blank=True), - max_size=8, + size=8, ), - max_size=8, + size=8, + ) + active_pieces = ArrayField( + models.CharField(max_length=10, blank=True), + max_size=32 ) Transformation of values between the database and the model, validation @@ -66,6 +71,18 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. If passed, the array will have a maximum size as specified, validated by forms and model validation, but not enforced by the database. + .. attribute:: size + + This is an optional argument. + + If passed, the array will have size as specified, validated + only by forms. + + .. note:: + + Defining both ``size`` and ``max_size`` will raise an exception. + Use ``size`` for fixed-length arrays and ``max_size`` for variable-length arrays with an upper limit. + Querying ``ArrayField`` ~~~~~~~~~~~~~~~~~~~~~~~ From f14d04a73978bdd9b4ed28bdce3dacdfaed9c0d3 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sun, 23 Mar 2025 22:21:49 -0300 Subject: [PATCH 03/16] Update doc --- docs/source/releases/5.1.x.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/releases/5.1.x.rst b/docs/source/releases/5.1.x.rst index 9690943e2..066f626c6 100644 --- a/docs/source/releases/5.1.x.rst +++ b/docs/source/releases/5.1.x.rst @@ -9,6 +9,8 @@ Django MongoDB Backend 5.1.x - Backward-incompatible: :class:`~django_mongodb_backend.fields.ArrayField`\'s ``size`` argument is renamed to ``max_size``. +- Added the ``size`` parameter to :class:`~django_mongodb_backend.fields.ArrayField` + for enforcing fixed-length arrays. - Added support for :doc:`database caching `. - Fixed ``QuerySet.raw_aggregate()`` field initialization when the document key order doesn't match the order of the model's fields. From 624cfca3d4aacc2db0d4fb522248fab7935dfe14 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sun, 23 Mar 2025 22:22:43 -0300 Subject: [PATCH 04/16] Move length validator --- django_mongodb_backend/fields/array.py | 5 ++--- django_mongodb_backend/fields/validators.py | 19 ------------------- django_mongodb_backend/validators.py | 19 ++++++++++++++++++- 3 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 django_mongodb_backend/fields/validators.py diff --git a/django_mongodb_backend/fields/array.py b/django_mongodb_backend/fields/array.py index e8178f562..e25aceda0 100644 --- a/django_mongodb_backend/fields/array.py +++ b/django_mongodb_backend/fields/array.py @@ -1,6 +1,5 @@ import json -from django.contrib.postgres.validators import ArrayMaxLengthValidator from django.core import checks, exceptions from django.db.models import DecimalField, Field, Func, IntegerField, Transform, Value from django.db.models.fields.mixins import CheckFieldDefaultMixin @@ -10,7 +9,7 @@ from ..forms import SimpleArrayField from ..query_utils import process_lhs, process_rhs from ..utils import prefix_validation_error -from .validators import LengthValidator +from ..validators import ArrayMaxLengthValidator, LengthValidator __all__ = ["ArrayField"] @@ -42,7 +41,6 @@ def __init__(self, base_field, max_size=None, size=None, **kwargs): if self.size: self.default_validators = [ *self.default_validators, - ArrayMaxLengthValidator(self.size), LengthValidator(self.size), ] # For performance, only add a from_db_value() method if the base field @@ -223,6 +221,7 @@ def formfield(self, **kwargs): "form_class": SimpleArrayField, "base_field": self.base_field.formfield(), "max_length": self.max_size, + "size": self.size, **kwargs, } ) diff --git a/django_mongodb_backend/fields/validators.py b/django_mongodb_backend/fields/validators.py deleted file mode 100644 index 1c6eb21bc..000000000 --- a/django_mongodb_backend/fields/validators.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.core.validators import BaseValidator -from django.utils.deconstruct import deconstructible -from django.utils.translation import ngettext_lazy - - -@deconstructible -class LengthValidator(BaseValidator): - message = ngettext_lazy( - "List contains %(show_value)d item, it should contain %(limit_value)d.", - "List contains %(show_value)d items, it should contain %(limit_value)d.", - "show_value", - ) - code = "length" - - def compare(self, a, b): - return a != b - - def clean(self, x): - return len(x) diff --git a/django_mongodb_backend/validators.py b/django_mongodb_backend/validators.py index 6005152e8..5ca6cbe23 100644 --- a/django_mongodb_backend/validators.py +++ b/django_mongodb_backend/validators.py @@ -1,4 +1,5 @@ -from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.core.validators import BaseValidator, MaxLengthValidator, MinLengthValidator +from django.utils.deconstruct import deconstructible from django.utils.translation import ngettext_lazy @@ -16,3 +17,19 @@ class ArrayMinLengthValidator(MinLengthValidator): "List contains %(show_value)d items, it should contain no fewer than %(limit_value)d.", "show_value", ) + + +@deconstructible +class LengthValidator(BaseValidator): + message = ngettext_lazy( + "List contains %(show_value)d item, it should contain %(limit_value)d.", + "List contains %(show_value)d items, it should contain %(limit_value)d.", + "show_value", + ) + code = "length" + + def compare(self, a, b): + return a != b + + def clean(self, x): + return len(x) From c45bc7701528b61e09f4461c8d100971f97221fa Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sun, 23 Mar 2025 22:27:03 -0300 Subject: [PATCH 05/16] Add size paramter --- django_mongodb_backend/forms/fields/array.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/django_mongodb_backend/forms/fields/array.py b/django_mongodb_backend/forms/fields/array.py index 0de48dff4..720768c24 100644 --- a/django_mongodb_backend/forms/fields/array.py +++ b/django_mongodb_backend/forms/fields/array.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from ...utils import prefix_validation_error -from ...validators import ArrayMaxLengthValidator, ArrayMinLengthValidator +from ...validators import ArrayMaxLengthValidator, ArrayMinLengthValidator, LengthValidator class SimpleArrayField(forms.CharField): @@ -14,7 +14,9 @@ class SimpleArrayField(forms.CharField): "item_invalid": _("Item %(nth)s in the array did not validate:"), } - def __init__(self, base_field, *, delimiter=",", max_length=None, min_length=None, **kwargs): + def __init__( + self, base_field, *, delimiter=",", max_length=None, min_length=None, size=None, **kwargs + ): self.base_field = base_field self.delimiter = delimiter super().__init__(**kwargs) @@ -24,6 +26,9 @@ def __init__(self, base_field, *, delimiter=",", max_length=None, min_length=Non if max_length is not None: self.max_length = max_length self.validators.append(ArrayMaxLengthValidator(int(max_length))) + if size is not None: + self.size = size + self.validators.append(LengthValidator(int(size))) def clean(self, value): value = super().clean(value) From 845d7fd8010e1e1ad2792860674c653fce5a65d3 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sun, 23 Mar 2025 22:27:33 -0300 Subject: [PATCH 06/16] Add unit tests --- tests/forms_tests_/test_array.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/forms_tests_/test_array.py b/tests/forms_tests_/test_array.py index e0107943a..51ef33065 100644 --- a/tests/forms_tests_/test_array.py +++ b/tests/forms_tests_/test_array.py @@ -134,6 +134,12 @@ def test_model_field_formfield_max_size(self): self.assertIsInstance(form_field, SimpleArrayField) self.assertEqual(form_field.max_length, 4) + def test_model_field_formfield_size(self): + model_field = ArrayField(models.CharField(max_length=27), size=4) + form_field = model_field.formfield() + self.assertIsInstance(form_field, SimpleArrayField) + self.assertEqual(form_field.size, 4) + def test_model_field_choices(self): model_field = ArrayField(models.IntegerField(choices=((1, "A"), (2, "B")))) form_field = model_field.formfield() From a90deb7e3c99bd3409827e827c6ec34eec87c5b4 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sun, 23 Mar 2025 22:53:00 -0300 Subject: [PATCH 07/16] Fix model field tests --- tests/model_fields_/array_index_migrations/0001_initial.py | 7 +++++++ tests/model_fields_/test_arrayfield.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/tests/model_fields_/array_index_migrations/0001_initial.py b/tests/model_fields_/array_index_migrations/0001_initial.py index 4113bdd41..dad86a15b 100644 --- a/tests/model_fields_/array_index_migrations/0001_initial.py +++ b/tests/model_fields_/array_index_migrations/0001_initial.py @@ -30,6 +30,13 @@ class Migration(migrations.Migration): "text", django_mongodb_backend.fields.ArrayField(models.TextField(), db_index=True), ), + ("char3", models.CharField(max_length=11, db_index=True)), + ( + "paragraph", + django_mongodb_backend.fields.ArrayField( + models.TextField(), size=10, db_index=True + ), + ), ], options={}, bases=(models.Model,), diff --git a/tests/model_fields_/test_arrayfield.py b/tests/model_fields_/test_arrayfield.py index 3149a5182..32f1463f2 100644 --- a/tests/model_fields_/test_arrayfield.py +++ b/tests/model_fields_/test_arrayfield.py @@ -764,7 +764,9 @@ def test_adding_arrayfield_with_index(self): ] self.assertIn("char", indexes) self.assertIn("char2", indexes) + self.assertIn("char3", indexes) self.assertIn("text", indexes) + self.assertIn("paragraph", indexes) call_command("migrate", "model_fields_", "zero", verbosity=0) self.assertNotIn(table_name, connection.introspection.table_names(None)) From 93432f9b6a9d29cc8fcffb77163dab9b4dbb443b Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Tue, 25 Mar 2025 01:16:29 -0300 Subject: [PATCH 08/16] Fix typo --- docs/source/ref/models/fields.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/ref/models/fields.rst b/docs/source/ref/models/fields.rst index 7941cca2b..84cad5087 100644 --- a/docs/source/ref/models/fields.rst +++ b/docs/source/ref/models/fields.rst @@ -13,7 +13,7 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. A field for storing lists of data. Most field types can be used, and you pass another field instance as the :attr:`base_field `. You may also specify a :attr:`max_size - ` and :attr:`size + ` or :attr:`size `. ``ArrayField`` can be nested to store multi-dimensional arrays. From 87162954317f15df01b146be6ed1d8dc194b0475 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Tue, 25 Mar 2025 01:17:27 -0300 Subject: [PATCH 09/16] Adjust unit tests --- tests/forms_tests_/test_array.py | 24 +++++++++++++++++++ .../array_index_migrations/0001_initial.py | 7 ------ tests/model_fields_/test_arrayfield.py | 2 -- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/forms_tests_/test_array.py b/tests/forms_tests_/test_array.py index 51ef33065..f5f7a9b3f 100644 --- a/tests/forms_tests_/test_array.py +++ b/tests/forms_tests_/test_array.py @@ -115,6 +115,30 @@ def test_min_length_singular(self): with self.assertRaisesMessage(exceptions.ValidationError, msg): field.clean([1]) + def test_size_length(self): + field = SimpleArrayField(forms.CharField(max_length=27), size=4) + with self.assertRaises(exceptions.ValidationError) as cm: + field.clean(["a", "b", "c"]) + self.assertEqual( + cm.exception.messages[0], + "List contains 3 items, it should contain 4.", + ) + with self.assertRaises(exceptions.ValidationError) as cm: + field.clean(["a", "b", "c", "d", "e"]) + self.assertEqual( + cm.exception.messages[0], + "List contains 5 items, it should contain 4.", + ) + + def test_size_length_singular(self): + field = SimpleArrayField(forms.CharField(max_length=27), size=4) + with self.assertRaises(exceptions.ValidationError) as cm: + field.clean(["a"]) + self.assertEqual( + cm.exception.messages[0], + "List contains 1 item, it should contain 4.", + ) + def test_required(self): field = SimpleArrayField(forms.CharField(), required=True) with self.assertRaises(exceptions.ValidationError) as cm: diff --git a/tests/model_fields_/array_index_migrations/0001_initial.py b/tests/model_fields_/array_index_migrations/0001_initial.py index dad86a15b..4113bdd41 100644 --- a/tests/model_fields_/array_index_migrations/0001_initial.py +++ b/tests/model_fields_/array_index_migrations/0001_initial.py @@ -30,13 +30,6 @@ class Migration(migrations.Migration): "text", django_mongodb_backend.fields.ArrayField(models.TextField(), db_index=True), ), - ("char3", models.CharField(max_length=11, db_index=True)), - ( - "paragraph", - django_mongodb_backend.fields.ArrayField( - models.TextField(), size=10, db_index=True - ), - ), ], options={}, bases=(models.Model,), diff --git a/tests/model_fields_/test_arrayfield.py b/tests/model_fields_/test_arrayfield.py index 32f1463f2..3149a5182 100644 --- a/tests/model_fields_/test_arrayfield.py +++ b/tests/model_fields_/test_arrayfield.py @@ -764,9 +764,7 @@ def test_adding_arrayfield_with_index(self): ] self.assertIn("char", indexes) self.assertIn("char2", indexes) - self.assertIn("char3", indexes) self.assertIn("text", indexes) - self.assertIn("paragraph", indexes) call_command("migrate", "model_fields_", "zero", verbosity=0) self.assertNotIn(table_name, connection.introspection.table_names(None)) From 5442d981b3310eb33e73b5fa1bb35932ef2ca9e6 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Tue, 25 Mar 2025 01:20:37 -0300 Subject: [PATCH 10/16] Add size and max_size check --- django_mongodb_backend/fields/array.py | 10 ++++++++-- tests/model_fields_/test_arrayfield.py | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/django_mongodb_backend/fields/array.py b/django_mongodb_backend/fields/array.py index e25aceda0..8f1c840a7 100644 --- a/django_mongodb_backend/fields/array.py +++ b/django_mongodb_backend/fields/array.py @@ -31,8 +31,6 @@ def __init__(self, base_field, max_size=None, size=None, **kwargs): self.base_field = base_field self.max_size = max_size self.size = size - if size and max_size: - raise ValueError("Cannot define both, size and max_size") if self.max_size: self.default_validators = [ *self.default_validators, @@ -106,6 +104,14 @@ def check(self, **kwargs): id="django_mongodb_backend.array.W004", ) ) + if self.size and self.max_size: + errors.append( + checks.Error( + "ArrayField cannot specify both size and max_size.", + obj=self, + id="django_mongodb_backend.array.E003", + ) + ) return errors def set_attributes_from_name(self, name): diff --git a/tests/model_fields_/test_arrayfield.py b/tests/model_fields_/test_arrayfield.py index 3149a5182..13aa2ce33 100644 --- a/tests/model_fields_/test_arrayfield.py +++ b/tests/model_fields_/test_arrayfield.py @@ -646,6 +646,15 @@ class MyModel(models.Model): self.assertEqual(len(errors), 1) self.assertEqual(errors[0].id, "django_mongodb_backend.array.E002") + def test_invalid_parameters(self): + class MyModel(models.Model): + field = ArrayField(models.CharField(max_length=3), size=3, max_size=4) + + model = MyModel() + errors = model.check() + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0].id, "django_mongodb_backend.array.E003") + def test_invalid_default(self): class MyModel(models.Model): field = ArrayField(models.IntegerField(), default=[]) From 759e251bb85530a731d81171e572925f4810ed2e Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Wed, 26 Mar 2025 08:24:12 -0300 Subject: [PATCH 11/16] Add ImproperlyConfigured exception --- django_mongodb_backend/forms/fields/array.py | 7 ++++++- tests/forms_tests_/test_array.py | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/django_mongodb_backend/forms/fields/array.py b/django_mongodb_backend/forms/fields/array.py index 720768c24..095c1ff5d 100644 --- a/django_mongodb_backend/forms/fields/array.py +++ b/django_mongodb_backend/forms/fields/array.py @@ -2,7 +2,7 @@ from itertools import chain from django import forms -from django.core.exceptions import ValidationError +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils.translation import gettext_lazy as _ from ...utils import prefix_validation_error @@ -20,6 +20,11 @@ def __init__( self.base_field = base_field self.delimiter = delimiter super().__init__(**kwargs) + if (min_length is not None or max_length is not None) and size is not None: + raise ImproperlyConfigured( + "SimpleArrayField param 'size' cannot be " + "specified with 'max_length' or 'min_length'." + ) if min_length is not None: self.min_length = min_length self.validators.append(ArrayMinLengthValidator(int(min_length))) diff --git a/tests/forms_tests_/test_array.py b/tests/forms_tests_/test_array.py index f5f7a9b3f..5cb392f8f 100644 --- a/tests/forms_tests_/test_array.py +++ b/tests/forms_tests_/test_array.py @@ -145,6 +145,15 @@ def test_required(self): field.clean("") self.assertEqual(cm.exception.messages[0], "This field is required.") + def test_misconfigured(self): + msg = "SimpleArrayField param 'size' cannot be specified with 'max_length' or 'min_length'." + with self.assertRaises(exceptions.ImproperlyConfigured) as cm: + SimpleArrayField(forms.CharField(), max_length=3, size=2) + self.assertEqual(cm.exception.args[0], msg) + with self.assertRaises(exceptions.ImproperlyConfigured) as cm: + SimpleArrayField(forms.CharField(), min_length=3, size=2) + self.assertEqual(cm.exception.args[0], msg) + def test_model_field_formfield(self): model_field = ArrayField(models.CharField(max_length=27)) form_field = model_field.formfield() From 4070897927bf0a08c07af69878c6c2806ba53739 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Wed, 26 Mar 2025 08:35:46 -0300 Subject: [PATCH 12/16] Update docs --- docs/source/ref/forms.rst | 9 +++++++++ docs/source/ref/models/fields.rst | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/source/ref/forms.rst b/docs/source/ref/forms.rst index 64c42755d..e0541d927 100644 --- a/docs/source/ref/forms.rst +++ b/docs/source/ref/forms.rst @@ -101,6 +101,15 @@ Stores an :class:`~bson.objectid.ObjectId`. This is an optional argument which validates that the array reaches at least the stated length. + .. attribute:: size + + This is an optional argument which validates that the array reaches at + exactly the stated length. + + .. note:: + Defining ``size`` along with ``max_length`` or ``min_length`` will raise an exception. + Use ``size`` for fixed-length arrays and ``max_length`` / ``min_length`` for variable-length arrays with an upper or lower limit. + .. admonition:: User friendly forms ``SimpleArrayField`` is not particularly user friendly in most cases, diff --git a/docs/source/ref/models/fields.rst b/docs/source/ref/models/fields.rst index 84cad5087..45d5d2a89 100644 --- a/docs/source/ref/models/fields.rst +++ b/docs/source/ref/models/fields.rst @@ -75,8 +75,7 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. This is an optional argument. - If passed, the array will have size as specified, validated - only by forms. + If passed, the array will have size as specified, validated by forms and model validation, but not enforced by the database. .. note:: From 83d279cef5fd23e2cee2712be7f446447500a453 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Wed, 26 Mar 2025 08:36:00 -0300 Subject: [PATCH 13/16] Rename test --- tests/model_fields_/test_arrayfield.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/model_fields_/test_arrayfield.py b/tests/model_fields_/test_arrayfield.py index 13aa2ce33..dc2d1254d 100644 --- a/tests/model_fields_/test_arrayfield.py +++ b/tests/model_fields_/test_arrayfield.py @@ -646,7 +646,7 @@ class MyModel(models.Model): self.assertEqual(len(errors), 1) self.assertEqual(errors[0].id, "django_mongodb_backend.array.E002") - def test_invalid_parameters(self): + def test_both_size_and_max_size(self): class MyModel(models.Model): field = ArrayField(models.CharField(max_length=3), size=3, max_size=4) From dff26acc4bb34262c4fe3acf7be736f0b049077c Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Wed, 26 Mar 2025 23:22:29 -0300 Subject: [PATCH 14/16] Check validator's exception message --- tests/validators_/tests.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/validators_/tests.py b/tests/validators_/tests.py index 2eb690abf..da12e2bf5 100644 --- a/tests/validators_/tests.py +++ b/tests/validators_/tests.py @@ -1,14 +1,25 @@ from django.core.exceptions import ValidationError from django.test import SimpleTestCase -from django_mongodb_backend.fields.validators import LengthValidator +from django_mongodb_backend.validators import LengthValidator class TestValidators(SimpleTestCase): def test_validators(self): validator = LengthValidator(10) - with self.assertRaises(ValidationError): + with self.assertRaises(ValidationError) as context_manager: validator([]) - with self.assertRaises(ValidationError): + self.assertEqual( + context_manager.exception.messages, ["List contains 0 items, it should contain 10."] + ) + with self.assertRaises(ValidationError) as context_manager: + validator([1]) + self.assertEqual( + context_manager.exception.messages, ["List contains 1 item, it should contain 10."] + ) + with self.assertRaises(ValidationError) as context_manager: validator(list(range(11))) + self.assertEqual( + context_manager.exception.messages, ["List contains 11 items, it should contain 10."] + ) self.assertEqual(validator(list(range(10))), None) From 437105765a2903355f582a013a43678360418442 Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Wed, 26 Mar 2025 23:28:44 -0300 Subject: [PATCH 15/16] Change SimpleArrayField size for length --- django_mongodb_backend/fields/array.py | 2 +- django_mongodb_backend/forms/fields/array.py | 12 +++++------ docs/source/ref/forms.rst | 22 +++++++++++--------- docs/source/ref/models/fields.rst | 4 ---- tests/forms_tests_/test_array.py | 14 +++++++------ 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/django_mongodb_backend/fields/array.py b/django_mongodb_backend/fields/array.py index 8f1c840a7..e21afdcfa 100644 --- a/django_mongodb_backend/fields/array.py +++ b/django_mongodb_backend/fields/array.py @@ -227,7 +227,7 @@ def formfield(self, **kwargs): "form_class": SimpleArrayField, "base_field": self.base_field.formfield(), "max_length": self.max_size, - "size": self.size, + "length": self.size, **kwargs, } ) diff --git a/django_mongodb_backend/forms/fields/array.py b/django_mongodb_backend/forms/fields/array.py index 095c1ff5d..47cbfccf1 100644 --- a/django_mongodb_backend/forms/fields/array.py +++ b/django_mongodb_backend/forms/fields/array.py @@ -15,14 +15,14 @@ class SimpleArrayField(forms.CharField): } def __init__( - self, base_field, *, delimiter=",", max_length=None, min_length=None, size=None, **kwargs + self, base_field, *, delimiter=",", max_length=None, min_length=None, length=None, **kwargs ): self.base_field = base_field self.delimiter = delimiter super().__init__(**kwargs) - if (min_length is not None or max_length is not None) and size is not None: + if (min_length is not None or max_length is not None) and length is not None: raise ImproperlyConfigured( - "SimpleArrayField param 'size' cannot be " + "SimpleArrayField param 'length' cannot be " "specified with 'max_length' or 'min_length'." ) if min_length is not None: @@ -31,9 +31,9 @@ def __init__( if max_length is not None: self.max_length = max_length self.validators.append(ArrayMaxLengthValidator(int(max_length))) - if size is not None: - self.size = size - self.validators.append(LengthValidator(int(size))) + if length is not None: + self.length = length + self.validators.append(LengthValidator(int(length))) def clean(self, value): value = super().clean(value) diff --git a/docs/source/ref/forms.rst b/docs/source/ref/forms.rst index e0541d927..1ce88beaa 100644 --- a/docs/source/ref/forms.rst +++ b/docs/source/ref/forms.rst @@ -33,7 +33,7 @@ Stores an :class:`~bson.objectid.ObjectId`. ``SimpleArrayField`` -------------------- -.. class:: SimpleArrayField(base_field, delimiter=',', max_length=None, min_length=None) +.. class:: SimpleArrayField(base_field, delimiter=',', length=None, max_length=None, min_length=None) A field which maps to an array. It is represented by an HTML ````. @@ -91,6 +91,17 @@ Stores an :class:`~bson.objectid.ObjectId`. in cases where the delimiter is a valid character in the underlying field. The delimiter does not need to be only one character. + .. attribute:: length + + This is an optional argument which validates that the array reaches at + exactly the stated length. + + .. note:: + Defining ``length`` along with ``max_length`` or ``min_length`` + will raise an exception. Use ``length`` for fixed-length arrays + and ``max_length`` / ``min_length`` for variable-length arrays + with an upper or lower limit. + .. attribute:: max_length This is an optional argument which validates that the array does not @@ -101,15 +112,6 @@ Stores an :class:`~bson.objectid.ObjectId`. This is an optional argument which validates that the array reaches at least the stated length. - .. attribute:: size - - This is an optional argument which validates that the array reaches at - exactly the stated length. - - .. note:: - Defining ``size`` along with ``max_length`` or ``min_length`` will raise an exception. - Use ``size`` for fixed-length arrays and ``max_length`` / ``min_length`` for variable-length arrays with an upper or lower limit. - .. admonition:: User friendly forms ``SimpleArrayField`` is not particularly user friendly in most cases, diff --git a/docs/source/ref/models/fields.rst b/docs/source/ref/models/fields.rst index 45d5d2a89..4952383fe 100644 --- a/docs/source/ref/models/fields.rst +++ b/docs/source/ref/models/fields.rst @@ -55,10 +55,6 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. ), size=8, ) - active_pieces = ArrayField( - models.CharField(max_length=10, blank=True), - max_size=32 - ) Transformation of values between the database and the model, validation of data and configuration, and serialization are all delegated to the diff --git a/tests/forms_tests_/test_array.py b/tests/forms_tests_/test_array.py index 5cb392f8f..6e26e0ded 100644 --- a/tests/forms_tests_/test_array.py +++ b/tests/forms_tests_/test_array.py @@ -116,7 +116,7 @@ def test_min_length_singular(self): field.clean([1]) def test_size_length(self): - field = SimpleArrayField(forms.CharField(max_length=27), size=4) + field = SimpleArrayField(forms.CharField(max_length=27), length=4) with self.assertRaises(exceptions.ValidationError) as cm: field.clean(["a", "b", "c"]) self.assertEqual( @@ -131,7 +131,7 @@ def test_size_length(self): ) def test_size_length_singular(self): - field = SimpleArrayField(forms.CharField(max_length=27), size=4) + field = SimpleArrayField(forms.CharField(max_length=27), length=4) with self.assertRaises(exceptions.ValidationError) as cm: field.clean(["a"]) self.assertEqual( @@ -146,12 +146,14 @@ def test_required(self): self.assertEqual(cm.exception.messages[0], "This field is required.") def test_misconfigured(self): - msg = "SimpleArrayField param 'size' cannot be specified with 'max_length' or 'min_length'." + msg = ( + "SimpleArrayField param 'length' cannot be specified with 'max_length' or 'min_length'." + ) with self.assertRaises(exceptions.ImproperlyConfigured) as cm: - SimpleArrayField(forms.CharField(), max_length=3, size=2) + SimpleArrayField(forms.CharField(), max_length=3, length=2) self.assertEqual(cm.exception.args[0], msg) with self.assertRaises(exceptions.ImproperlyConfigured) as cm: - SimpleArrayField(forms.CharField(), min_length=3, size=2) + SimpleArrayField(forms.CharField(), min_length=3, length=2) self.assertEqual(cm.exception.args[0], msg) def test_model_field_formfield(self): @@ -171,7 +173,7 @@ def test_model_field_formfield_size(self): model_field = ArrayField(models.CharField(max_length=27), size=4) form_field = model_field.formfield() self.assertIsInstance(form_field, SimpleArrayField) - self.assertEqual(form_field.size, 4) + self.assertEqual(form_field.length, 4) def test_model_field_choices(self): model_field = ArrayField(models.IntegerField(choices=((1, "A"), (2, "B")))) From 9428d2e98c444c0b04226c0f057068c7f584078e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 28 Mar 2025 20:07:52 -0400 Subject: [PATCH 16/16] edits --- django_mongodb_backend/forms/fields/array.py | 4 +- docs/source/ref/forms.rst | 11 ++--- docs/source/ref/models/fields.rst | 18 ++++---- docs/source/releases/5.1.x.rst | 6 +-- tests/forms_tests_/test_array.py | 34 +++++---------- tests/model_fields_/test_arrayfield.py | 7 +--- tests/validators_/tests.py | 44 +++++++++++--------- 7 files changed, 54 insertions(+), 70 deletions(-) diff --git a/django_mongodb_backend/forms/fields/array.py b/django_mongodb_backend/forms/fields/array.py index 47cbfccf1..854508cc5 100644 --- a/django_mongodb_backend/forms/fields/array.py +++ b/django_mongodb_backend/forms/fields/array.py @@ -21,9 +21,9 @@ def __init__( self.delimiter = delimiter super().__init__(**kwargs) if (min_length is not None or max_length is not None) and length is not None: + invalid_param = "max_length" if max_length is not None else "min_length" raise ImproperlyConfigured( - "SimpleArrayField param 'length' cannot be " - "specified with 'max_length' or 'min_length'." + f"The length and {invalid_param} parameters are mutually exclusive." ) if min_length is not None: self.min_length = min_length diff --git a/docs/source/ref/forms.rst b/docs/source/ref/forms.rst index 1ce88beaa..934af20e3 100644 --- a/docs/source/ref/forms.rst +++ b/docs/source/ref/forms.rst @@ -93,14 +93,11 @@ Stores an :class:`~bson.objectid.ObjectId`. .. attribute:: length - This is an optional argument which validates that the array reaches at - exactly the stated length. + This is an optional argument which validates that the array contains + the stated number of items. - .. note:: - Defining ``length`` along with ``max_length`` or ``min_length`` - will raise an exception. Use ``length`` for fixed-length arrays - and ``max_length`` / ``min_length`` for variable-length arrays - with an upper or lower limit. + ``length`` may not be specified along with ``max_length`` or + ``min_length``. .. attribute:: max_length diff --git a/docs/source/ref/models/fields.rst b/docs/source/ref/models/fields.rst index 4952383fe..47a3149c6 100644 --- a/docs/source/ref/models/fields.rst +++ b/docs/source/ref/models/fields.rst @@ -11,11 +11,9 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. .. class:: ArrayField(base_field, max_size=None, size=None, **options) A field for storing lists of data. Most field types can be used, and you - pass another field instance as the :attr:`base_field - `. You may also specify a :attr:`max_size - ` or :attr:`size - `. ``ArrayField`` can be nested to store - multi-dimensional arrays. + pass another field instance as the :attr:`~ArrayField.base_field`. You may + also specify a :attr:`~ArrayField.size` or :attr:`~ArrayField.max_size`. + ``ArrayField`` can be nested to store multi-dimensional arrays. If you give the field a :attr:`~django.db.models.Field.default`, ensure it's a callable such as ``list`` (for an empty default) or a callable that @@ -67,16 +65,14 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. If passed, the array will have a maximum size as specified, validated by forms and model validation, but not enforced by the database. + The ``max_size`` and ``size`` options are mutually exclusive. + .. attribute:: size This is an optional argument. - If passed, the array will have size as specified, validated by forms and model validation, but not enforced by the database. - - .. note:: - - Defining both ``size`` and ``max_size`` will raise an exception. - Use ``size`` for fixed-length arrays and ``max_size`` for variable-length arrays with an upper limit. + If passed, the array will have size as specified, validated by forms + and model validation, but not enforced by the database. Querying ``ArrayField`` ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/releases/5.1.x.rst b/docs/source/releases/5.1.x.rst index 066f626c6..2e36e6d8d 100644 --- a/docs/source/releases/5.1.x.rst +++ b/docs/source/releases/5.1.x.rst @@ -8,9 +8,9 @@ Django MongoDB Backend 5.1.x *Unreleased* - Backward-incompatible: :class:`~django_mongodb_backend.fields.ArrayField`\'s - ``size`` argument is renamed to ``max_size``. -- Added the ``size`` parameter to :class:`~django_mongodb_backend.fields.ArrayField` - for enforcing fixed-length arrays. + :attr:`~.ArrayField.size` parameter is renamed to + :attr:`~.ArrayField.max_size`. The :attr:`~.ArrayField.size` parameter is now + used to enforce fixed-length arrays. - Added support for :doc:`database caching `. - Fixed ``QuerySet.raw_aggregate()`` field initialization when the document key order doesn't match the order of the model's fields. diff --git a/tests/forms_tests_/test_array.py b/tests/forms_tests_/test_array.py index 6e26e0ded..1a496019e 100644 --- a/tests/forms_tests_/test_array.py +++ b/tests/forms_tests_/test_array.py @@ -117,27 +117,18 @@ def test_min_length_singular(self): def test_size_length(self): field = SimpleArrayField(forms.CharField(max_length=27), length=4) - with self.assertRaises(exceptions.ValidationError) as cm: + msg = "List contains 3 items, it should contain 4." + with self.assertRaisesMessage(exceptions.ValidationError, msg): field.clean(["a", "b", "c"]) - self.assertEqual( - cm.exception.messages[0], - "List contains 3 items, it should contain 4.", - ) - with self.assertRaises(exceptions.ValidationError) as cm: + msg = "List contains 5 items, it should contain 4." + with self.assertRaisesMessage(exceptions.ValidationError, msg): field.clean(["a", "b", "c", "d", "e"]) - self.assertEqual( - cm.exception.messages[0], - "List contains 5 items, it should contain 4.", - ) def test_size_length_singular(self): field = SimpleArrayField(forms.CharField(max_length=27), length=4) - with self.assertRaises(exceptions.ValidationError) as cm: + msg = "List contains 1 item, it should contain 4." + with self.assertRaisesMessage(exceptions.ValidationError, msg): field.clean(["a"]) - self.assertEqual( - cm.exception.messages[0], - "List contains 1 item, it should contain 4.", - ) def test_required(self): field = SimpleArrayField(forms.CharField(), required=True) @@ -145,16 +136,13 @@ def test_required(self): field.clean("") self.assertEqual(cm.exception.messages[0], "This field is required.") - def test_misconfigured(self): - msg = ( - "SimpleArrayField param 'length' cannot be specified with 'max_length' or 'min_length'." - ) - with self.assertRaises(exceptions.ImproperlyConfigured) as cm: + def test_length_and_max_min_length(self): + msg = "The length and max_length parameters are mutually exclusive." + with self.assertRaisesMessage(exceptions.ImproperlyConfigured, msg): SimpleArrayField(forms.CharField(), max_length=3, length=2) - self.assertEqual(cm.exception.args[0], msg) - with self.assertRaises(exceptions.ImproperlyConfigured) as cm: + msg = "The length and min_length parameters are mutually exclusive." + with self.assertRaisesMessage(exceptions.ImproperlyConfigured, msg): SimpleArrayField(forms.CharField(), min_length=3, length=2) - self.assertEqual(cm.exception.args[0], msg) def test_model_field_formfield(self): model_field = ArrayField(models.CharField(max_length=27)) diff --git a/tests/model_fields_/test_arrayfield.py b/tests/model_fields_/test_arrayfield.py index dc2d1254d..08d1d8eee 100644 --- a/tests/model_fields_/test_arrayfield.py +++ b/tests/model_fields_/test_arrayfield.py @@ -830,12 +830,9 @@ def test_with_max_size_singular(self): def test_with_size(self): field = ArrayField(models.IntegerField(), size=3) field.clean([1, 2, 3], None) - with self.assertRaises(exceptions.ValidationError) as cm: + msg = "List contains 4 items, it should contain 3." + with self.assertRaisesMessage(exceptions.ValidationError, msg): field.clean([1, 2, 3, 4], None) - self.assertEqual( - cm.exception.messages[0], - "List contains 4 items, it should contain 3.", - ) def test_with_size_singular(self): field = ArrayField(models.IntegerField(), size=2) diff --git a/tests/validators_/tests.py b/tests/validators_/tests.py index da12e2bf5..09a549490 100644 --- a/tests/validators_/tests.py +++ b/tests/validators_/tests.py @@ -4,22 +4,28 @@ from django_mongodb_backend.validators import LengthValidator -class TestValidators(SimpleTestCase): - def test_validators(self): - validator = LengthValidator(10) - with self.assertRaises(ValidationError) as context_manager: - validator([]) - self.assertEqual( - context_manager.exception.messages, ["List contains 0 items, it should contain 10."] - ) - with self.assertRaises(ValidationError) as context_manager: - validator([1]) - self.assertEqual( - context_manager.exception.messages, ["List contains 1 item, it should contain 10."] - ) - with self.assertRaises(ValidationError) as context_manager: - validator(list(range(11))) - self.assertEqual( - context_manager.exception.messages, ["List contains 11 items, it should contain 10."] - ) - self.assertEqual(validator(list(range(10))), None) +class TestLengthValidator(SimpleTestCase): + validator = LengthValidator(10) + + def test_empty(self): + msg = "List contains 0 items, it should contain 10." + with self.assertRaisesMessage(ValidationError, msg): + self.validator([]) + + def test_singular(self): + msg = "List contains 1 item, it should contain 10." + with self.assertRaisesMessage(ValidationError, msg): + self.validator([1]) + + def test_too_short(self): + msg = "List contains 9 items, it should contain 10." + with self.assertRaisesMessage(ValidationError, msg): + self.validator([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + def test_too_long(self): + msg = "List contains 11 items, it should contain 10." + with self.assertRaisesMessage(ValidationError, msg): + self.validator(list(range(11))) + + def test_valid(self): + self.assertEqual(self.validator(list(range(10))), None)