diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index e9e1de24..1cf5ec3c 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -86,6 +86,10 @@ expressions: * ``istartswith`` -- string field starts with value (case insensitive) * ``endswith`` -- string field ends with value * ``iendswith`` -- string field ends with value (case insensitive) +* ``wholeword`` -- string field contains whole word +* ``iwholeword`` -- string field contains whole word (case insensitive) +* ``regex`` -- string field match by regex +* ``iregex`` -- string field match by regex (case insensitive) * ``match`` -- performs an $elemMatch so you can match an entire document within an array diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 25bf37ef..b9c49553 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -157,10 +157,17 @@ class StringField(BaseField): regex = r"%s$" elif op == "exact": regex = r"^%s$" + elif op == "wholeword": + regex = r"\b%s\b" + elif op == "regex": + regex = value # escape unsafe characters which could lead to a re.error - value = re.escape(value) - value = re.compile(regex % value, flags) + if op == "regex": + value = re.compile(regex, flags) + else: + value = re.escape(value) + value = re.compile(regex % value, flags) return super().prepare_query_value(op, value) @@ -1086,16 +1093,7 @@ class DictField(ComplexBaseField): return DictField(db_field=member_name) def prepare_query_value(self, op, value): - match_operators = [ - "contains", - "icontains", - "startswith", - "istartswith", - "endswith", - "iendswith", - "exact", - "iexact", - ] + match_operators = [*STRING_OPERATORS] if op in match_operators and isinstance(value, str): return StringField().prepare_query_value(op, value) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 4c3d051b..cea2ef4c 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -51,6 +51,10 @@ STRING_OPERATORS = ( "iendswith", "exact", "iexact", + "regex", + "iregex", + "wholeword", + "iwholeword", ) CUSTOM_OPERATORS = ("match",) MATCH_OPERATORS = ( diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 5add0750..9a2f2862 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -1255,6 +1255,34 @@ class TestQueryset(unittest.TestCase): obj = self.Person.objects(name__iexact="gUIDO VAN rOSSU").first() assert obj is None + # Test wholeword + obj = self.Person.objects(name__wholeword="Guido").first() + assert obj == person + obj = self.Person.objects(name__wholeword="rossum").first() + assert obj is None + obj = self.Person.objects(name__wholeword="Rossu").first() + assert obj is None + + # Test iwholeword + obj = self.Person.objects(name__iwholeword="rOSSUM").first() + assert obj == person + obj = self.Person.objects(name__iwholeword="rOSSU").first() + assert obj is None + + # Test regex + obj = self.Person.objects(name__regex="^[Guido].*[Rossum]$").first() + assert obj == person + obj = self.Person.objects(name__regex="^[guido].*[rossum]$").first() + assert obj is None + obj = self.Person.objects(name__regex="^[uido].*[Rossum]$").first() + assert obj is None + + # Test iregex + obj = self.Person.objects(name__iregex="^[guido].*[rossum]$").first() + assert obj == person + obj = self.Person.objects(name__iregex="^[Uido].*[Rossum]$").first() + assert obj is None + # Test unsafe expressions person = self.Person(name="Guido van Rossum [.'Geek']") person.save() @@ -1339,7 +1367,14 @@ class TestQueryset(unittest.TestCase): person.save() people = self.Person.objects - people = people.filter(name__startswith="Gui").filter(name__not__endswith="tum") + people = ( + people.filter(name__startswith="Gui") + .filter(name__not__endswith="tum") + .filter(name__icontains="VAN") + .filter(name__regex="^Guido") + .filter(name__wholeword="Guido") + .filter(name__wholeword="van") + ) assert people.count() == 1 def assertSequence(self, qs, expected):