Skip to content

Commit f95b8b4

Browse files
committed
fix ObjectId in forms
1 parent e8a0d96 commit f95b8b4

File tree

9 files changed

+105
-23
lines changed

9 files changed

+105
-23
lines changed

.github/workflows/runtests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"force_insert_update",
5757
"foreign_object",
5858
"forms_tests",
59+
"forms_tests_",
5960
"from_db_value",
6061
"generic_inline_admin",
6162
"generic_relations",

django_mongodb/fields/objectid.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from django.db.models.fields import Field
44
from django.utils.translation import gettext_lazy as _
55

6+
from django_mongodb import forms
7+
68

79
class ObjectIdMixin:
810
default_error_messages = {
9-
"invalid": _("“%(value)s” value must be an Object Id."),
11+
"invalid": _("“%(value)s” is not a valid Object Id."),
1012
}
1113
description = _("Object Id")
1214

@@ -17,12 +19,8 @@ def rel_db_type(self, connection):
1719
return "objectId"
1820

1921
def get_prep_value(self, value):
20-
if value is None or isinstance(value, ObjectId):
21-
return value
22-
try:
23-
return ObjectId(value)
24-
except (errors.InvalidId, TypeError) as e:
25-
raise ValueError(f"Field '{self.name}' expected an ObjectId but got {value!r}.") from e
22+
value = super().get_prep_value(value)
23+
return self.to_python(value)
2624

2725
def to_python(self, value):
2826
if value is None:
@@ -36,6 +34,14 @@ def to_python(self, value):
3634
params={"value": value},
3735
) from None
3836

37+
def formfield(self, **kwargs):
38+
return super().formfield(
39+
**{
40+
"form_class": forms.ObjectIdField,
41+
**kwargs,
42+
}
43+
)
44+
3945

4046
class ObjectIdField(ObjectIdMixin, Field):
4147
def deconstruct(self):

django_mongodb/forms/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .fields import ObjectIdField
2+
3+
__all__ = ["ObjectIdField"]

django_mongodb/forms/fields.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from bson import ObjectId
2+
from bson.errors import InvalidId
3+
from django.core.exceptions import ValidationError
4+
from django.forms import Field
5+
from django.utils.translation import gettext_lazy as _
6+
7+
8+
class ObjectIdField(Field):
9+
default_error_messages = {
10+
"invalid": _("Enter a valid Object Id."),
11+
}
12+
13+
def prepare_value(self, value):
14+
if isinstance(value, ObjectId):
15+
return str(value)
16+
return value
17+
18+
def to_python(self, value):
19+
value = super().to_python(value)
20+
if value in self.empty_values:
21+
return None
22+
if not isinstance(value, ObjectId):
23+
try:
24+
value = ObjectId(value)
25+
except InvalidId:
26+
raise ValidationError(self.error_messages["invalid"], code="invalid") from None
27+
return value

docs/source/forms.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Forms API reference
2+
===================
3+
4+
.. module:: django_mongodb.forms
5+
6+
Some MongoDB-specific fields are available in ``django_mongodb.forms``.
7+
8+
``ObjectIdField``
9+
-----------------
10+
11+
.. class:: ObjectIdField
12+
13+
Stores an :class:`~bson.objectid.ObjectId`.

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ django-mongodb 5.0.x documentation
77

88
fields
99
querysets
10+
forms
1011

1112
Indices and tables
1213
==================

tests/forms_tests_/__init__.py

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from bson import ObjectId
2+
from django.core.exceptions import ValidationError
3+
from django.test import SimpleTestCase
4+
5+
from django_mongodb.forms.fields import ObjectIdField
6+
7+
8+
class ObjectIdFieldTests(SimpleTestCase):
9+
def test_clean(self):
10+
field = ObjectIdField()
11+
value = field.clean("675747ec45260945758d76bc")
12+
self.assertEqual(value, ObjectId("675747ec45260945758d76bc"))
13+
14+
def test_clean_objectid(self):
15+
field = ObjectIdField()
16+
value = field.clean(ObjectId("675747ec45260945758d76bc"))
17+
self.assertEqual(value, ObjectId("675747ec45260945758d76bc"))
18+
19+
def test_clean_empty_string(self):
20+
field = ObjectIdField(required=False)
21+
value = field.clean("")
22+
self.assertEqual(value, None)
23+
24+
def test_clean_invalid(self):
25+
field = ObjectIdField()
26+
with self.assertRaises(ValidationError) as cm:
27+
field.clean("invalid")
28+
self.assertEqual(cm.exception.messages[0], "Enter a valid Object Id.")
29+
30+
def test_prepare_value(self):
31+
field = ObjectIdField()
32+
value = field.prepare_value(ObjectId("675747ec45260945758d76bc"))
33+
self.assertEqual(value, "675747ec45260945758d76bc")

tests/model_fields_/test_objectidfield.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.core.exceptions import ValidationError
33
from django.test import SimpleTestCase, TestCase
44

5+
from django_mongodb import forms
56
from django_mongodb.fields import ObjectIdField
67

78
from .models import NullableObjectIdModel, ObjectIdModel, PrimaryKeyObjectIdModel
@@ -15,6 +16,10 @@ def test_deconstruct(self):
1516
self.assertEqual(args, [])
1617
self.assertEqual(kwargs, {})
1718

19+
def test_formfield(self):
20+
f = ObjectIdField().formfield()
21+
self.assertIsInstance(f, forms.ObjectIdField)
22+
1823
def test_get_internal_type(self):
1924
f = ObjectIdField()
2025
self.assertEqual(f.get_internal_type(), "ObjectIdField")
@@ -32,9 +37,9 @@ def test_to_python_null(self):
3237

3338
def test_to_python_invalid_value(self):
3439
f = ObjectIdField()
35-
for invalid_value in ["None", {}, [], 123]:
40+
for invalid_value in ["None", "", {}, [], 123]:
3641
with self.subTest(invalid_value=invalid_value):
37-
msg = f"['“{invalid_value}value must be an Object Id.']"
42+
msg = f"['“{invalid_value}is not a valid Object Id.']"
3843
with self.assertRaisesMessage(ValidationError, msg):
3944
f.to_python(invalid_value)
4045

@@ -46,22 +51,16 @@ def test_get_prep_value_objectid(self):
4651
value = ObjectId("1" * 24)
4752
self.assertEqual(ObjectIdField().get_prep_value(value), value)
4853

49-
def test_get_prep_value_empty(self):
50-
# This is necessary to allow an empty ObjectIdField to be saved in
51-
# forms, unless we add an ObjectId form field to do the conversion (see
52-
# UUIDField for an example).
53-
self.assertIsNone(ObjectIdField().get_prep_value(""))
54-
5554
def test_get_prep_value_null(self):
5655
self.assertIsNone(ObjectIdField().get_prep_value(None))
5756

5857
def test_get_prep_value_invalid_values(self):
5958
f = ObjectIdField()
6059
f.name = "test"
61-
for invalid_value in ["None", {}, [], 123]:
60+
for invalid_value in ["None", "", {}, [], 123]:
6261
with self.subTest(invalid_value=invalid_value):
63-
msg = f"Field 'test' expected an ObjectId but got {invalid_value!r}."
64-
with self.assertRaisesMessage(ValueError, msg):
62+
msg = f"['“{invalid_value}” is not a valid Object Id.']"
63+
with self.assertRaisesMessage(ValidationError, msg):
6564
f.get_prep_value(invalid_value)
6665

6766

@@ -82,17 +81,16 @@ def test_null_handling(self):
8281
self.assertIsNone(loaded.field)
8382

8483
def test_pk_validated(self):
85-
# See https://code.djangoproject.com/ticket/24859
86-
with self.assertRaisesMessage(TypeError, "must be an Object Id."):
84+
with self.assertRaisesMessage(ValidationError, "is not a valid Object Id."):
8785
PrimaryKeyObjectIdModel.objects.get(pk={})
8886

89-
with self.assertRaisesMessage(TypeError, "must be an Object Id."):
87+
with self.assertRaisesMessage(ValidationError, "is not a valid Object Id."):
9088
PrimaryKeyObjectIdModel.objects.get(pk=[])
9189

9290
def test_wrong_value(self):
9391
# Copied from UUID tests. Raises ValueError, might be okay.
94-
with self.assertRaisesMessage(ValidationError, "must be an Object Id."):
92+
with self.assertRaisesMessage(ValidationError, "is not a valid Object Id."):
9593
ObjectIdModel.objects.get(field="not-a-objectid")
9694

97-
with self.assertRaisesMessage(ValidationError, "must be an Object Id."):
95+
with self.assertRaisesMessage(ValidationError, "is not a valid Object Id."):
9896
ObjectIdModel.objects.create(field="not-a-objectid")

0 commit comments

Comments
 (0)