Skip to content

Rename ArrayField.size to max_size #273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions django_mongodb_backend/fields/array.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +9,7 @@
from ..forms import SimpleArrayField
from ..query_utils import process_lhs, process_rhs
from ..utils import prefix_validation_error
from ..validators import ArrayMaxLengthValidator, LengthValidator

__all__ = ["ArrayField"]

Expand All @@ -27,13 +27,19 @@ class ArrayField(CheckFieldDefaultMixin, Field):
}
_default_hint = ("list", "[]")

def __init__(self, base_field, 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 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.
Expand Down Expand Up @@ -98,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 have both size and max_size.",
obj=self,
id="django_mongodb_backend.array.E003",
)
)
return errors

def set_attributes_from_name(self, name):
Expand All @@ -124,12 +138,11 @@ def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if path == "django_mongodb_backend.fields.array.ArrayField":
path = "django_mongodb_backend.fields.ArrayField"
kwargs.update(
{
"base_field": self.base_field.clone(),
"size": self.size,
}
)
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):
Expand Down Expand Up @@ -213,7 +226,8 @@ def formfield(self, **kwargs):
**{
"form_class": SimpleArrayField,
"base_field": self.base_field.formfield(),
"max_length": self.size,
"max_length": self.max_size,
"length": self.size,
**kwargs,
}
)
Expand Down
16 changes: 13 additions & 3 deletions django_mongodb_backend/forms/fields/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,38 @@
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
from ...validators import ArrayMaxLengthValidator, ArrayMinLengthValidator
from ...validators import ArrayMaxLengthValidator, ArrayMinLengthValidator, LengthValidator


class SimpleArrayField(forms.CharField):
default_error_messages = {
"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, 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 length is not None:
invalid_param = "max_length" if max_length is not None else "min_length"
raise ImproperlyConfigured(
f"The length and {invalid_param} parameters are mutually exclusive."
)
if min_length is not None:
self.min_length = min_length
self.validators.append(ArrayMinLengthValidator(int(min_length)))
if max_length is not None:
self.max_length = max_length
self.validators.append(ArrayMaxLengthValidator(int(max_length)))
if length is not None:
self.length = length
self.validators.append(LengthValidator(int(length)))

def clean(self, value):
value = super().clean(value)
Expand Down
19 changes: 18 additions & 1 deletion django_mongodb_backend/validators.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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)
10 changes: 9 additions & 1 deletion docs/source/ref/forms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<input>``.

Expand Down Expand Up @@ -91,6 +91,14 @@ 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 contains
the stated number of items.

``length`` may not be specified along with ``max_length`` or
``min_length``.

.. attribute:: max_length

This is an optional argument which validates that the array does not
Expand Down
26 changes: 17 additions & 9 deletions docs/source/ref/models/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``.
``ArrayField``
--------------

.. class:: ArrayField(base_field, 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
<ArrayField.base_field>`. You may also specify a :attr:`size
<ArrayField.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
Expand Down Expand Up @@ -59,12 +58,21 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``.
of data and configuration, and serialization are all delegated to the
underlying base field.

.. attribute:: size
.. attribute:: max_size

This is an optional argument.

If passed, the array will have a maximum size as specified, validated
only by forms.
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.

Querying ``ArrayField``
~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -168,8 +176,8 @@ Index transforms
^^^^^^^^^^^^^^^^

Index transforms index into the array. Any non-negative integer can be used.
There are no errors if it exceeds the :attr:`size <ArrayField.size>` of the
array. The lookups available after the transform are those from the
There are no errors if it exceeds the :attr:`max_size <ArrayField.max_size>` of
the array. The lookups available after the transform are those from the
:attr:`base_field <ArrayField.base_field>`. For example:

.. code-block:: pycon
Expand Down
4 changes: 4 additions & 0 deletions docs/source/releases/5.1.x.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Django MongoDB Backend 5.1.x

*Unreleased*

- Backward-incompatible: :class:`~django_mongodb_backend.fields.ArrayField`\'s
: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 </topics/cache>`.
- Fixed ``QuerySet.raw_aggregate()`` field initialization when the document key
order doesn't match the order of the model's fields.
Expand Down
70 changes: 42 additions & 28 deletions tests/forms_tests_/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,15 @@ def test_valid(self):

def test_to_python_fail(self):
field = SimpleArrayField(forms.IntegerField())
with self.assertRaises(exceptions.ValidationError) as cm:
msg = "Item 1 in the array did not validate: Enter a whole number."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean("a,b,9")
self.assertEqual(
cm.exception.messages[0],
"Item 1 in the array did not validate: Enter a whole number.",
)

def test_validate_fail(self):
field = SimpleArrayField(forms.CharField(required=True))
with self.assertRaises(exceptions.ValidationError) as cm:
msg = "Item 3 in the array did not validate: This field is required."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean("a,b,")
self.assertEqual(
cm.exception.messages[0],
"Item 3 in the array did not validate: This field is required.",
)

def test_validate_fail_base_field_error_params(self):
field = SimpleArrayField(forms.CharField(max_length=2))
Expand Down Expand Up @@ -68,12 +62,9 @@ def test_validate_fail_base_field_error_params(self):

def test_validators_fail(self):
field = SimpleArrayField(forms.RegexField("[a-e]{2}"))
with self.assertRaises(exceptions.ValidationError) as cm:
msg = "Item 1 in the array did not validate: Enter a valid value."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean("a,bc,de")
self.assertEqual(
cm.exception.messages[0],
"Item 1 in the array did not validate: Enter a valid value.",
)

def test_delimiter(self):
field = SimpleArrayField(forms.CharField(), delimiter="|")
Expand All @@ -92,21 +83,15 @@ def test_prepare_value(self):

def test_max_length(self):
field = SimpleArrayField(forms.CharField(), max_length=2)
with self.assertRaises(exceptions.ValidationError) as cm:
msg = "List contains 3 items, it should contain no more than 2."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean("a,b,c")
self.assertEqual(
cm.exception.messages[0],
"List contains 3 items, it should contain no more than 2.",
)

def test_min_length(self):
field = SimpleArrayField(forms.CharField(), min_length=4)
with self.assertRaises(exceptions.ValidationError) as cm:
msg = "List contains 3 items, it should contain no fewer than 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 no fewer than 4.",
)

def test_min_length_singular(self):
field = SimpleArrayField(forms.IntegerField(), min_length=2)
Expand All @@ -115,11 +100,34 @@ 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), length=4)
msg = "List contains 3 items, it should contain 4."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean(["a", "b", "c"])
msg = "List contains 5 items, it should contain 4."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean(["a", "b", "c", "d", "e"])

def test_size_length_singular(self):
field = SimpleArrayField(forms.CharField(max_length=27), length=4)
msg = "List contains 1 item, it should contain 4."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean(["a"])

def test_required(self):
field = SimpleArrayField(forms.CharField(), required=True)
with self.assertRaises(exceptions.ValidationError) as cm:
msg = "This field is required."
with self.assertRaisesMessage(exceptions.ValidationError, msg):
field.clean("")
self.assertEqual(cm.exception.messages[0], "This field is required.")

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)
msg = "The length and min_length parameters are mutually exclusive."
with self.assertRaisesMessage(exceptions.ImproperlyConfigured, msg):
SimpleArrayField(forms.CharField(), min_length=3, length=2)

def test_model_field_formfield(self):
model_field = ArrayField(models.CharField(max_length=27))
Expand All @@ -128,11 +136,17 @@ def test_model_field_formfield(self):
self.assertIsInstance(form_field.base_field, forms.CharField)
self.assertEqual(form_field.base_field.max_length, 27)

def test_model_field_formfield_max_size(self):
model_field = ArrayField(models.CharField(max_length=27), max_size=4)
form_field = model_field.formfield()
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.max_length, 4)
self.assertEqual(form_field.length, 4)

def test_model_field_choices(self):
model_field = ArrayField(models.IntegerField(choices=((1, "A"), (2, "B"))))
Expand Down
3 changes: 1 addition & 2 deletions tests/forms_tests_/test_objectidfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ def test_clean_empty_string(self):

def test_clean_invalid(self):
field = ObjectIdField()
with self.assertRaises(ValidationError) as cm:
with self.assertRaisesMessage(ValidationError, "Enter a valid Object Id."):
field.clean("invalid")
self.assertEqual(cm.exception.messages[0], "Enter a valid Object Id.")

def test_prepare_value(self):
field = ObjectIdField()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Migration(migrations.Migration):
),
(
"field",
django_mongodb_backend.fields.ArrayField(models.IntegerField(), size=None),
django_mongodb_backend.fields.ArrayField(models.IntegerField()),
),
],
options={},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="integerarraydefaultmodel",
name="field_2",
field=django_mongodb_backend.fields.ArrayField(
models.IntegerField(), default=[], size=None
),
field=django_mongodb_backend.fields.ArrayField(models.IntegerField(), default=[]),
preserve_default=False,
),
]
2 changes: 1 addition & 1 deletion tests/model_fields_/array_index_migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Migration(migrations.Migration):
(
"char",
django_mongodb_backend.fields.ArrayField(
models.CharField(max_length=10), db_index=True, size=100
models.CharField(max_length=10), db_index=True, max_size=100
),
),
("char2", models.CharField(max_length=11, db_index=True)),
Expand Down
Loading
Loading