Repository: MongoEngine/mongoengine

Issue:
Queryset update doesn't go through field validation
Here's an example:

```
import mongoengine as me
me.connect('test')

class AgeField(me.IntField):
    def validate(self, value):
        if value < 0:
            self.error('must be larger than zero')

class Person(me.Document):
    age = AgeField()

try:
    Person(age=-2).save()
except me.ValidationError:
    pass
else:
    1/0
p = Person(age=100).save()
Person.objects.update(set__age=-1)
print Person.objects()[0].age # prints -1
```

I found a nice solution for this, not sure if it's right though:

```
    def prepare_query_value(self, op, value):
        self.validate(value)
        return super(AgeField, self).prepare_query_value(op, value)
```



Discussion:
As this is a breaking change - I'm marking this down for 0.9

What is the status of this? Validators not being run means that also the regex-validation is not run which is very annoying

Any updates or plans for this one? This will be very useful. Thanks!

Just an FYI - calling `update()` not only skips validation, but does not throw any of the related signals.


Resolving patch:
diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py
--- a/mongoengine/base/fields.py
+++ b/mongoengine/base/fields.py
@@ -17,6 +17,11 @@
            "ObjectIdField", "GeoJsonBaseField")
 
 
+UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push',
+                        'push_all', 'pull', 'pull_all', 'add_to_set',
+                        'set_on_insert', 'min', 'max'])
+
+
 class BaseField(object):
 
     """A base class for fields in a MongoDB document. Instances of this class
@@ -150,6 +155,8 @@ def to_mongo(self, value):
     def prepare_query_value(self, op, value):
         """Prepare a value that is being used in a query for PyMongo.
         """
+        if op in UPDATE_OPERATORS:
+            self.validate(value)
         return value
 
     def validate(self, value, clean=True):
diff --git a/mongoengine/fields.py b/mongoengine/fields.py
--- a/mongoengine/fields.py
+++ b/mongoengine/fields.py
@@ -108,7 +108,7 @@ def prepare_query_value(self, op, value):
             # escape unsafe characters which could lead to a re.error
             value = re.escape(value)
             value = re.compile(regex % value, flags)
-        return value
+        return super(StringField, self).prepare_query_value(op, value)
 
 
 class URLField(StringField):
@@ -203,7 +203,7 @@ def prepare_query_value(self, op, value):
         if value is None:
             return value
 
-        return int(value)
+        return super(IntField, self).prepare_query_value(op, int(value))
 
 
 class LongField(BaseField):
@@ -238,7 +238,7 @@ def prepare_query_value(self, op, value):
         if value is None:
             return value
 
-        return long(value)
+        return super(LongField, self).prepare_query_value(op, long(value))
 
 
 class FloatField(BaseField):
@@ -273,7 +273,7 @@ def prepare_query_value(self, op, value):
         if value is None:
             return value
 
-        return float(value)
+        return super(FloatField, self).prepare_query_value(op, float(value))
 
 
 class DecimalField(BaseField):
@@ -347,7 +347,7 @@ def validate(self, value):
             self.error('Decimal value is too large')
 
     def prepare_query_value(self, op, value):
-        return self.to_mongo(value)
+        return super(DecimalField, self).prepare_query_value(op, self.to_mongo(value))
 
 
 class BooleanField(BaseField):
@@ -434,7 +434,7 @@ def to_mongo(self, value):
                     return None
 
     def prepare_query_value(self, op, value):
-        return self.to_mongo(value)
+        return super(DateTimeField, self).prepare_query_value(op, self.to_mongo(value))
 
 
 class ComplexDateTimeField(StringField):
@@ -518,7 +518,7 @@ def to_mongo(self, value):
         return self._convert_from_datetime(value)
 
     def prepare_query_value(self, op, value):
-        return self._convert_from_datetime(value)
+        return super(ComplexDateTimeField, self).prepare_query_value(op, self._convert_from_datetime(value))
 
 
 class EmbeddedDocumentField(BaseField):
@@ -569,6 +569,9 @@ def lookup_member(self, member_name):
         return self.document_type._fields.get(member_name)
 
     def prepare_query_value(self, op, value):
+        if not isinstance(value, self.document_type):
+            value = self.document_type._from_son(value)
+        super(EmbeddedDocumentField, self).prepare_query_value(op, value)
         return self.to_mongo(value)
 
 
@@ -585,7 +588,7 @@ class GenericEmbeddedDocumentField(BaseField):
     """
 
     def prepare_query_value(self, op, value):
-        return self.to_mongo(value)
+        return super(GenericEmbeddedDocumentField, self).prepare_query_value(op, self.to_mongo(value))
 
     def to_python(self, value):
         if isinstance(value, dict):
@@ -668,7 +671,8 @@ def prepare_query_value(self, op, value):
         if isinstance(value, basestring):
             from mongoengine.fields import StringField
             return StringField().prepare_query_value(op, value)
-        return self.to_mongo(value)
+        return super(DynamicField, self).prepare_query_value(op, self.to_mongo(value))
+
 
     def validate(self, value, clean=True):
         if hasattr(value, "validate"):
@@ -979,8 +983,10 @@ def to_python(self, value):
     def prepare_query_value(self, op, value):
         if value is None:
             return None
+        super(ReferenceField, self).prepare_query_value(op, value)
         return self.to_mongo(value)
 
+
     def validate(self, value):
 
         if not isinstance(value, (self.document_type, DBRef)):
diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py
--- a/mongoengine/queryset/transform.py
+++ b/mongoengine/queryset/transform.py
@@ -3,6 +3,7 @@
 import pymongo
 from bson import SON
 
+from mongoengine.base.fields import UPDATE_OPERATORS
 from mongoengine.connection import get_connection
 from mongoengine.common import _import_class
 from mongoengine.errors import InvalidQueryError, LookUpError
@@ -24,10 +25,6 @@
 MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
                    STRING_OPERATORS + CUSTOM_OPERATORS)
 
-UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push',
-                    'push_all', 'pull', 'pull_all', 'add_to_set',
-                    'set_on_insert', 'min', 'max')
-
 
 def query(_doc_cls=None, _field_operation=False, **query):
     """Transform a query from Django-style format to Mongo format.


Test patch:
diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py
--- a/tests/queryset/queryset.py
+++ b/tests/queryset/queryset.py
@@ -602,6 +602,20 @@ def test_update_results(self):
             set__name="bobby", multi=True)
         self.assertEqual(result, 2)
 
+    def test_update_validate(self):
+        class EmDoc(EmbeddedDocument):
+            str_f = StringField()
+
+        class Doc(Document):
+            str_f = StringField()
+            dt_f = DateTimeField()
+            cdt_f = ComplexDateTimeField()
+            ed_f = EmbeddedDocumentField(EmDoc)
+
+        self.assertRaises(ValidationError, Doc.objects().update, str_f=1, upsert=True)
+        self.assertRaises(ValidationError, Doc.objects().update, dt_f="datetime", upsert=True)
+        self.assertRaises(ValidationError, Doc.objects().update, ed_f__str_f=1, upsert=True)
+
     def test_upsert(self):
         self.Person.drop_collection()