diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 6dc35c30..f5c70728 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -426,19 +426,6 @@ either a single field name, or a list or tuple of field names:: first_name = StringField() last_name = StringField(unique_with='first_name') -Skipping Document validation on save ------------------------------------- -You can also skip the whole document validation process by setting -``validate=False`` when calling the :meth:`~mongoengine.document.Document.save` -method:: - - class Recipient(Document): - name = StringField() - email = EmailField() - - recipient = Recipient(name='admin', email='root@localhost') - recipient.save() # will raise a ValidationError while - recipient.save(validate=False) # won't Document collections ==================== diff --git a/docs/guide/document-instances.rst b/docs/guide/document-instances.rst index 64f17c08..5dacc0dd 100644 --- a/docs/guide/document-instances.rst +++ b/docs/guide/document-instances.rst @@ -41,35 +41,6 @@ already exist, then any changes will be updated atomically. For example:: .. seealso:: :ref:`guide-atomic-updates` -Pre save data validation and cleaning -------------------------------------- -MongoEngine allows you to create custom cleaning rules for your documents when -calling :meth:`~mongoengine.Document.save`. By providing a custom -:meth:`~mongoengine.Document.clean` method you can do any pre validation / data -cleaning. - -This might be useful if you want to ensure a default value based on other -document values for example:: - - class Essay(Document): - status = StringField(choices=('Published', 'Draft'), required=True) - pub_date = DateTimeField() - - def clean(self): - """Ensures that only published essays have a `pub_date` and - automatically sets `pub_date` if essay is published and `pub_date` - is not set""" - if self.status == 'Draft' and self.pub_date is not None: - msg = 'Draft entries should not have a publication date.' - raise ValidationError(msg) - # Set the pub_date for published items if not set. - if self.status == 'Published' and self.pub_date is None: - self.pub_date = datetime.now() - -.. note:: - Cleaning is only called if validation is turned on and when calling - :meth:`~mongoengine.Document.save`. - Cascading Saves --------------- If your document contains :class:`~mongoengine.fields.ReferenceField` or diff --git a/docs/guide/index.rst b/docs/guide/index.rst index a0364ec1..95a9b92b 100644 --- a/docs/guide/index.rst +++ b/docs/guide/index.rst @@ -10,6 +10,7 @@ User Guide defining-documents document-instances querying + validation gridfs signals text-indexes diff --git a/docs/guide/validation.rst b/docs/guide/validation.rst new file mode 100644 index 00000000..971f6eb5 --- /dev/null +++ b/docs/guide/validation.rst @@ -0,0 +1,123 @@ +==================== +Document Validation +==================== + +By design, MongoEngine strictly validates the documents right before they are inserted in MongoDB +and makes sure they are consistent with the fields defined in your models. + +MongoEngine makes the assumption that the documents that exists in the DB are compliant with the schema. +This means that Mongoengine will not validate a document when an object is loaded from the DB into an instance +of your model but this operation may fail under some circumstances (e.g. if there is a field in +the document fetched from the database that is not defined in your model). + + +Built-in validation +=================== + +Mongoengine provides different fields that encapsulate the corresponding validation +out of the box. Validation runs when calling `.validate()` or `.save()` + +.. code-block:: python + + from mongoengine import Document, EmailField + + class User(Document): + email = EmailField() + age = IntField(min_value=0, max_value=99) + + user = User(email='invalid@', age=24) + user.validate() # raises ValidationError (Invalid email address: ['email']) + user.save() # raises ValidationError (Invalid email address: ['email']) + + user2 = User(email='john.doe@garbage.com', age=1000) + user2.save() # raises ValidationError (Integer value is too large: ['age']) + +Custom validation +================= + +The following feature can be used to customize the validation: + +* Field `validation` parameter + +.. code-block:: python + + def not_john_doe(name): + if name == 'John Doe': + raise ValidationError("John Doe is not a valid name") + + class Person(Document): + full_name = StringField(validation=not_john_doe) + + Person(full_name='Billy Doe').save() + Person(full_name='John Doe').save() # raises ValidationError (John Doe is not a valid name) + + +* Document `clean` method + +This method is called as part of :meth:`~mongoengine.document.Document.save` and should be used to provide +custom model validation and/or to modify some of the field values prior to validation. +For instance, you could use it to automatically provide a value for a field, or to do validation +that requires access to more than a single field. + +.. code-block:: python + + class Essay(Document): + status = StringField(choices=('Published', 'Draft'), required=True) + pub_date = DateTimeField() + + def clean(self): + # Validate that only published essays have a `pub_date` + if self.status == 'Draft' and self.pub_date is not None: + raise ValidationError('Draft entries should not have a publication date.') + # Set the pub_date for published items if not set. + if self.status == 'Published' and self.pub_date is None: + self.pub_date = datetime.now() + +.. note:: + Cleaning is only called if validation is turned on and when calling + :meth:`~mongoengine.Document.save`. + +* Adding custom Field classes + +We recommend as much as possible to use fields provided by MongoEngine. However, it is also possible +to subclass a Field and encapsulate some validation by overriding the `validate` method + +.. code-block:: python + + class AgeField(IntField): + + def validate(self, value): + super(AgeField, self).validate(value) # let IntField.validate run first + if value == 60: + self.error('60 is not allowed') + + class Person(Document): + age = AgeField(min_value=0, max_value=99) + + Person(age=20).save() # passes + Person(age=1000).save() # raises ValidationError (Integer value is too large: ['age']) + Person(age=60).save() # raises ValidationError (Person:None) (60 is not allowed: ['age']) + + +.. note:: + + When overriding `validate`, use `self.error("your-custom-error")` instead of raising ValidationError explicitly, + it will provide a better context with the error message + +Skipping validation +==================== + +Although discouraged as it allows to violate fields constraints, if for some reason you need to disable +the validation and cleaning of a document when you call :meth:`~mongoengine.document.Document.save`, you can use `.save(validate=False)`. + +.. code-block:: python + + class Person(Document): + age = IntField(max_value=100) + + Person(age=1000).save() # raises ValidationError (Integer value is too large) + + Person(age=1000).save(validate=False) + person = Person.objects.first() + assert person.age == 1000 + diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 8c69cc73..2a8fde6d 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -215,7 +215,7 @@ class EmbeddedDocumentList(BaseList): Filters the list by only including embedded documents with the given keyword arguments. - This method only supports simple comparison (e.g: .filter(name='John Doe')) + This method only supports simple comparison (e.g. .filter(name='John Doe')) and does not support operators like __gte, __lte, __icontains like queryset.filter does :param kwargs: The keyword arguments corresponding to the fields to diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index e697fe40..0bab2886 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -314,7 +314,8 @@ class BaseDocument: def clean(self): """ - Hook for doing document level data cleaning before validation is run. + Hook for doing document level data cleaning (usually validation or assignment) + before validation is run. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the diff --git a/mongoengine/fields.py b/mongoengine/fields.py index bba05ea7..c5926cbd 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -433,7 +433,7 @@ class DecimalField(BaseField): :param max_value: Validation rule for the maximum acceptable value. :param force_string: Store the value as a string (instead of a float). Be aware that this affects query sorting and operation like lte, gte (as string comparison is applied) - and some query operator won't work (e.g: inc, dec) + and some query operator won't work (e.g. inc, dec) :param precision: Number of decimal places to store. :param rounding: The rounding rule from the python decimal library: diff --git a/tests/utils.py b/tests/utils.py index 7ee22c3c..195b9dba 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -50,7 +50,7 @@ def _decorated_with_ver_requirement(func, mongo_version_req, oper): ran against MongoDB < v3.6. :param mongo_version_req: The mongodb version requirement (tuple(int, int)) - :param oper: The operator to apply (e.g: operator.ge) + :param oper: The operator to apply (e.g. operator.ge) """ def _inner(*args, **kwargs):