Skip to content

Commit a1c6b39

Browse files
committed
add field name validation to EmbeddedModelField lookups
1 parent 5156577 commit a1c6b39

File tree

2 files changed

+72
-5
lines changed

2 files changed

+72
-5
lines changed

django_mongodb_backend/fields/embedded_model.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import difflib
2+
13
from django.core import checks
4+
from django.core.exceptions import FieldDoesNotExist
25
from django.db import models
36
from django.db.models.fields.related import lazy_related_operation
47
from django.db.models.lookups import Transform
@@ -123,7 +126,8 @@ def get_transform(self, name):
123126
transform = super().get_transform(name)
124127
if transform:
125128
return transform
126-
return KeyTransformFactory(name)
129+
field = self.embedded_model._meta.get_field(name)
130+
return KeyTransformFactory(name, field)
127131

128132
def validate(self, value, model_instance):
129133
super().validate(value, model_instance)
@@ -145,9 +149,36 @@ def formfield(self, **kwargs):
145149

146150

147151
class KeyTransform(Transform):
148-
def __init__(self, key_name, *args, **kwargs):
152+
def __init__(self, key_name, ref_field, *args, **kwargs):
149153
super().__init__(*args, **kwargs)
150154
self.key_name = str(key_name)
155+
self.ref_field = ref_field
156+
157+
def get_transform(self, name):
158+
"""
159+
Validate that `name` is either a field of an embedded model or a
160+
lookup on an embedded model's field.
161+
"""
162+
result = None
163+
if isinstance(self.ref_field, EmbeddedModelField):
164+
opts = self.ref_field.embedded_model._meta
165+
new_field = opts.get_field(name)
166+
result = KeyTransformFactory(name, new_field)
167+
else:
168+
if self.ref_field.get_transform(name) is None:
169+
suggested_lookups = difflib.get_close_matches(name, self.ref_field.get_lookups())
170+
if suggested_lookups:
171+
suggested_lookups = " or ".join(suggested_lookups)
172+
suggestion = f", perhaps you meant {suggested_lookups}?"
173+
else:
174+
suggestion = "."
175+
raise FieldDoesNotExist(
176+
f"Unsupported lookup '{name}' for "
177+
f"{self.ref_field.__class__.__name__} '{self.ref_field.name}'"
178+
f"{suggestion}"
179+
)
180+
result = KeyTransformFactory(name, self.ref_field)
181+
return result
151182

152183
def preprocess_lhs(self, compiler, connection):
153184
key_transforms = [self.key_name]
@@ -165,8 +196,9 @@ def as_mql(self, compiler, connection):
165196

166197

167198
class KeyTransformFactory:
168-
def __init__(self, key_name):
199+
def __init__(self, key_name, ref_field):
169200
self.key_name = key_name
201+
self.ref_field = ref_field
170202

171203
def __call__(self, *args, **kwargs):
172-
return KeyTransform(self.key_name, *args, **kwargs)
204+
return KeyTransform(self.key_name, self.ref_field, *args, **kwargs)

tests/model_fields_/test_embedded_model.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import operator
22

3-
from django.core.exceptions import ValidationError
3+
from django.core.exceptions import FieldDoesNotExist, ValidationError
44
from django.db import models
55
from django.db.models import ExpressionWrapper, F, Max, Sum
66
from django.test import SimpleTestCase, TestCase
@@ -147,6 +147,41 @@ def test_nested(self):
147147
self.assertCountEqual(Book.objects.filter(author__address__city="NYC"), [obj])
148148

149149

150+
class InvalidLookupTests(SimpleTestCase):
151+
def test_invalid_field(self):
152+
msg = "Author has no field named 'first_name'"
153+
with self.assertRaisesMessage(FieldDoesNotExist, msg):
154+
Book.objects.filter(author__first_name="Bob")
155+
156+
def test_invalid_field_nested(self):
157+
msg = "Address has no field named 'floor'"
158+
with self.assertRaisesMessage(FieldDoesNotExist, msg):
159+
Book.objects.filter(author__address__floor="NYC")
160+
161+
def test_invalid_lookup(self):
162+
msg = "Unsupported lookup 'foo' for CharField 'city'."
163+
with self.assertRaisesMessage(FieldDoesNotExist, msg):
164+
Book.objects.filter(author__address__city__foo="NYC")
165+
166+
def test_invalid_lookup_with_suggestions(self):
167+
msg = (
168+
"Unsupported lookup '{lookup}' for CharField 'name', "
169+
"perhaps you meant {suggested_lookups}?"
170+
)
171+
with self.assertRaisesMessage(
172+
FieldDoesNotExist, msg.format(lookup="exactly", suggested_lookups="exact or iexact")
173+
):
174+
Book.objects.filter(author__name__exactly="NYC")
175+
with self.assertRaisesMessage(
176+
FieldDoesNotExist, msg.format(lookup="gti", suggested_lookups="gt or gte")
177+
):
178+
Book.objects.filter(author__name__gti="NYC")
179+
with self.assertRaisesMessage(
180+
FieldDoesNotExist, msg.format(lookup="is_null", suggested_lookups="isnull")
181+
):
182+
Book.objects.filter(author__name__is_null="NYC")
183+
184+
150185
@isolate_apps("model_fields_")
151186
class CheckTests(SimpleTestCase):
152187
def test_no_relational_fields(self):

0 commit comments

Comments
 (0)