Skip to content

Commit d0ce345

Browse files
WaVEVtimgraham
authored andcommitted
Re-add ArrayField.size (now for fixed length validation)
1 parent 98c57ae commit d0ce345

File tree

10 files changed

+161
-15
lines changed

10 files changed

+161
-15
lines changed

django_mongodb_backend/fields/array.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22

3-
from django.contrib.postgres.validators import ArrayMaxLengthValidator
43
from django.core import checks, exceptions
54
from django.db.models import DecimalField, Field, Func, IntegerField, Transform, Value
65
from django.db.models.fields.mixins import CheckFieldDefaultMixin
@@ -10,6 +9,7 @@
109
from ..forms import SimpleArrayField
1110
from ..query_utils import process_lhs, process_rhs
1211
from ..utils import prefix_validation_error
12+
from ..validators import ArrayMaxLengthValidator, LengthValidator
1313

1414
__all__ = ["ArrayField"]
1515

@@ -27,14 +27,20 @@ class ArrayField(CheckFieldDefaultMixin, Field):
2727
}
2828
_default_hint = ("list", "[]")
2929

30-
def __init__(self, base_field, max_size=None, **kwargs):
30+
def __init__(self, base_field, max_size=None, size=None, **kwargs):
3131
self.base_field = base_field
3232
self.max_size = max_size
33+
self.size = size
3334
if self.max_size:
3435
self.default_validators = [
3536
*self.default_validators,
3637
ArrayMaxLengthValidator(self.max_size),
3738
]
39+
if self.size:
40+
self.default_validators = [
41+
*self.default_validators,
42+
LengthValidator(self.size),
43+
]
3844
# For performance, only add a from_db_value() method if the base field
3945
# implements it.
4046
if hasattr(self.base_field, "from_db_value"):
@@ -98,6 +104,14 @@ def check(self, **kwargs):
98104
id="django_mongodb_backend.array.W004",
99105
)
100106
)
107+
if self.size and self.max_size:
108+
errors.append(
109+
checks.Error(
110+
"ArrayField cannot have both size and max_size.",
111+
obj=self,
112+
id="django_mongodb_backend.array.E003",
113+
)
114+
)
101115
return errors
102116

103117
def set_attributes_from_name(self, name):
@@ -127,6 +141,8 @@ def deconstruct(self):
127141
kwargs["base_field"] = self.base_field.clone()
128142
if self.max_size is not None:
129143
kwargs["max_size"] = self.max_size
144+
if self.size is not None:
145+
kwargs["size"] = self.size
130146
return name, path, args, kwargs
131147

132148
def to_python(self, value):
@@ -211,6 +227,7 @@ def formfield(self, **kwargs):
211227
"form_class": SimpleArrayField,
212228
"base_field": self.base_field.formfield(),
213229
"max_length": self.max_size,
230+
"length": self.size,
214231
**kwargs,
215232
}
216233
)

django_mongodb_backend/forms/fields/array.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,38 @@
22
from itertools import chain
33

44
from django import forms
5-
from django.core.exceptions import ValidationError
5+
from django.core.exceptions import ImproperlyConfigured, ValidationError
66
from django.utils.translation import gettext_lazy as _
77

88
from ...utils import prefix_validation_error
9-
from ...validators import ArrayMaxLengthValidator, ArrayMinLengthValidator
9+
from ...validators import ArrayMaxLengthValidator, ArrayMinLengthValidator, LengthValidator
1010

1111

1212
class SimpleArrayField(forms.CharField):
1313
default_error_messages = {
1414
"item_invalid": _("Item %(nth)s in the array did not validate:"),
1515
}
1616

17-
def __init__(self, base_field, *, delimiter=",", max_length=None, min_length=None, **kwargs):
17+
def __init__(
18+
self, base_field, *, delimiter=",", max_length=None, min_length=None, length=None, **kwargs
19+
):
1820
self.base_field = base_field
1921
self.delimiter = delimiter
2022
super().__init__(**kwargs)
23+
if (min_length is not None or max_length is not None) and length is not None:
24+
invalid_param = "max_length" if max_length is not None else "min_length"
25+
raise ImproperlyConfigured(
26+
f"The length and {invalid_param} parameters are mutually exclusive."
27+
)
2128
if min_length is not None:
2229
self.min_length = min_length
2330
self.validators.append(ArrayMinLengthValidator(int(min_length)))
2431
if max_length is not None:
2532
self.max_length = max_length
2633
self.validators.append(ArrayMaxLengthValidator(int(max_length)))
34+
if length is not None:
35+
self.length = length
36+
self.validators.append(LengthValidator(int(length)))
2737

2838
def clean(self, value):
2939
value = super().clean(value)

django_mongodb_backend/validators.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from django.core.validators import MaxLengthValidator, MinLengthValidator
1+
from django.core.validators import BaseValidator, MaxLengthValidator, MinLengthValidator
2+
from django.utils.deconstruct import deconstructible
23
from django.utils.translation import ngettext_lazy
34

45

@@ -16,3 +17,19 @@ class ArrayMinLengthValidator(MinLengthValidator):
1617
"List contains %(show_value)d items, it should contain no fewer than %(limit_value)d.",
1718
"show_value",
1819
)
20+
21+
22+
@deconstructible
23+
class LengthValidator(BaseValidator):
24+
message = ngettext_lazy(
25+
"List contains %(show_value)d item, it should contain %(limit_value)d.",
26+
"List contains %(show_value)d items, it should contain %(limit_value)d.",
27+
"show_value",
28+
)
29+
code = "length"
30+
31+
def compare(self, a, b):
32+
return a != b
33+
34+
def clean(self, x):
35+
return len(x)

docs/source/ref/forms.rst

+9-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Stores an :class:`~bson.objectid.ObjectId`.
3333
``SimpleArrayField``
3434
--------------------
3535

36-
.. class:: SimpleArrayField(base_field, delimiter=',', max_length=None, min_length=None)
36+
.. class:: SimpleArrayField(base_field, delimiter=',', length=None, max_length=None, min_length=None)
3737

3838
A field which maps to an array. It is represented by an HTML ``<input>``.
3939

@@ -91,6 +91,14 @@ Stores an :class:`~bson.objectid.ObjectId`.
9191
in cases where the delimiter is a valid character in the underlying
9292
field. The delimiter does not need to be only one character.
9393

94+
.. attribute:: length
95+
96+
This is an optional argument which validates that the array contains
97+
the stated number of items.
98+
99+
``length`` may not be specified along with ``max_length`` or
100+
``min_length``.
101+
94102
.. attribute:: max_length
95103

96104
This is an optional argument which validates that the array does not

docs/source/ref/models/fields.rst

+15-7
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``.
88
``ArrayField``
99
--------------
1010

11-
.. class:: ArrayField(base_field, max_size=None, **options)
11+
.. class:: ArrayField(base_field, max_size=None, size=None, **options)
1212

1313
A field for storing lists of data. Most field types can be used, and you
14-
pass another field instance as the :attr:`base_field
15-
<ArrayField.base_field>`. You may also specify a :attr:`max_size
16-
<ArrayField.max_size>`. ``ArrayField`` can be nested to store
17-
multi-dimensional arrays.
14+
pass another field instance as the :attr:`~ArrayField.base_field`. You may
15+
also specify a :attr:`~ArrayField.size` or :attr:`~ArrayField.max_size`.
16+
``ArrayField`` can be nested to store multi-dimensional arrays.
1817

1918
If you give the field a :attr:`~django.db.models.Field.default`, ensure
2019
it's a callable such as ``list`` (for an empty default) or a callable that
@@ -50,9 +49,9 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``.
5049
board = ArrayField(
5150
ArrayField(
5251
models.CharField(max_length=10, blank=True),
53-
max_size=8,
52+
size=8,
5453
),
55-
max_size=8,
54+
size=8,
5655
)
5756

5857
Transformation of values between the database and the model, validation
@@ -66,6 +65,15 @@ Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``.
6665
If passed, the array will have a maximum size as specified, validated
6766
by forms and model validation, but not enforced by the database.
6867

68+
The ``max_size`` and ``size`` options are mutually exclusive.
69+
70+
.. attribute:: size
71+
72+
This is an optional argument.
73+
74+
If passed, the array will have size as specified, validated by forms
75+
and model validation, but not enforced by the database.
76+
6977
Querying ``ArrayField``
7078
~~~~~~~~~~~~~~~~~~~~~~~
7179

docs/source/releases/5.1.x.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ Django MongoDB Backend 5.1.x
88
*Unreleased*
99

1010
- Backward-incompatible: :class:`~django_mongodb_backend.fields.ArrayField`\'s
11-
``size`` argument is renamed to ``max_size``.
11+
:attr:`~.ArrayField.size` parameter is renamed to
12+
:attr:`~.ArrayField.max_size`. The :attr:`~.ArrayField.size` parameter is now
13+
used to enforce fixed-length arrays.
1214
- Added support for :doc:`database caching </topics/cache>`.
1315
- Fixed ``QuerySet.raw_aggregate()`` field initialization when the document key
1416
order doesn't match the order of the model's fields.

tests/forms_tests_/test_array.py

+29
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,35 @@ def test_min_length_singular(self):
100100
with self.assertRaisesMessage(exceptions.ValidationError, msg):
101101
field.clean([1])
102102

103+
def test_size_length(self):
104+
field = SimpleArrayField(forms.CharField(max_length=27), length=4)
105+
msg = "List contains 3 items, it should contain 4."
106+
with self.assertRaisesMessage(exceptions.ValidationError, msg):
107+
field.clean(["a", "b", "c"])
108+
msg = "List contains 5 items, it should contain 4."
109+
with self.assertRaisesMessage(exceptions.ValidationError, msg):
110+
field.clean(["a", "b", "c", "d", "e"])
111+
112+
def test_size_length_singular(self):
113+
field = SimpleArrayField(forms.CharField(max_length=27), length=4)
114+
msg = "List contains 1 item, it should contain 4."
115+
with self.assertRaisesMessage(exceptions.ValidationError, msg):
116+
field.clean(["a"])
117+
103118
def test_required(self):
104119
field = SimpleArrayField(forms.CharField(), required=True)
105120
msg = "This field is required."
106121
with self.assertRaisesMessage(exceptions.ValidationError, msg):
107122
field.clean("")
108123

124+
def test_length_and_max_min_length(self):
125+
msg = "The length and max_length parameters are mutually exclusive."
126+
with self.assertRaisesMessage(exceptions.ImproperlyConfigured, msg):
127+
SimpleArrayField(forms.CharField(), max_length=3, length=2)
128+
msg = "The length and min_length parameters are mutually exclusive."
129+
with self.assertRaisesMessage(exceptions.ImproperlyConfigured, msg):
130+
SimpleArrayField(forms.CharField(), min_length=3, length=2)
131+
109132
def test_model_field_formfield(self):
110133
model_field = ArrayField(models.CharField(max_length=27))
111134
form_field = model_field.formfield()
@@ -119,6 +142,12 @@ def test_model_field_formfield_max_size(self):
119142
self.assertIsInstance(form_field, SimpleArrayField)
120143
self.assertEqual(form_field.max_length, 4)
121144

145+
def test_model_field_formfield_size(self):
146+
model_field = ArrayField(models.CharField(max_length=27), size=4)
147+
form_field = model_field.formfield()
148+
self.assertIsInstance(form_field, SimpleArrayField)
149+
self.assertEqual(form_field.length, 4)
150+
122151
def test_model_field_choices(self):
123152
model_field = ArrayField(models.IntegerField(choices=((1, "A"), (2, "B"))))
124153
form_field = model_field.formfield()

tests/model_fields_/test_arrayfield.py

+24
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,16 @@ class MyModel(models.Model):
646646
self.assertEqual(len(errors), 1)
647647
self.assertEqual(errors[0].id, "django_mongodb_backend.array.E002")
648648

649+
def test_both_size_and_max_size(self):
650+
class MyModel(models.Model):
651+
field = ArrayField(models.CharField(max_length=3), size=3, max_size=4)
652+
653+
model = MyModel()
654+
errors = model.check()
655+
self.assertEqual(len(errors), 1)
656+
self.assertEqual(errors[0].id, "django_mongodb_backend.array.E003")
657+
self.assertEqual(errors[0].msg, "ArrayField cannot have both size and max_size.")
658+
649659
def test_invalid_default(self):
650660
class MyModel(models.Model):
651661
field = ArrayField(models.IntegerField(), default=[])
@@ -812,6 +822,20 @@ def test_with_max_size_singular(self):
812822
with self.assertRaisesMessage(exceptions.ValidationError, msg):
813823
field.clean([1, 2], None)
814824

825+
def test_with_size(self):
826+
field = ArrayField(models.IntegerField(), size=3)
827+
field.clean([1, 2, 3], None)
828+
msg = "List contains 4 items, it should contain 3."
829+
with self.assertRaisesMessage(exceptions.ValidationError, msg):
830+
field.clean([1, 2, 3, 4], None)
831+
832+
def test_with_size_singular(self):
833+
field = ArrayField(models.IntegerField(), size=2)
834+
field.clean([1, 2], None)
835+
msg = "List contains 1 item, it should contain 2."
836+
with self.assertRaisesMessage(exceptions.ValidationError, msg):
837+
field.clean([1], None)
838+
815839
def test_nested_array_mismatch(self):
816840
field = ArrayField(ArrayField(models.IntegerField()))
817841
field.clean([[1, 2], [3, 4]], None)

tests/validators_/__init__.py

Whitespace-only changes.

tests/validators_/test_length.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from django.core.exceptions import ValidationError
2+
from django.test import SimpleTestCase
3+
4+
from django_mongodb_backend.validators import LengthValidator
5+
6+
7+
class TestLengthValidator(SimpleTestCase):
8+
validator = LengthValidator(10)
9+
10+
def test_empty(self):
11+
msg = "List contains 0 items, it should contain 10."
12+
with self.assertRaisesMessage(ValidationError, msg):
13+
self.validator([])
14+
15+
def test_singular(self):
16+
msg = "List contains 1 item, it should contain 10."
17+
with self.assertRaisesMessage(ValidationError, msg):
18+
self.validator([1])
19+
20+
def test_too_short(self):
21+
msg = "List contains 9 items, it should contain 10."
22+
with self.assertRaisesMessage(ValidationError, msg):
23+
self.validator([1, 2, 3, 4, 5, 6, 7, 8, 9])
24+
25+
def test_too_long(self):
26+
msg = "List contains 11 items, it should contain 10."
27+
with self.assertRaisesMessage(ValidationError, msg):
28+
self.validator(list(range(11)))
29+
30+
def test_valid(self):
31+
self.assertEqual(self.validator(list(range(10))), None)

0 commit comments

Comments
 (0)