From 13d8dfdb5fc44ce92ed3373111d4e2e74cf2e66b Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 26 Apr 2013 08:43:38 +0000 Subject: [PATCH 01/40] Save py2.6 from Decimal Float fun --- mongoengine/fields.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 2e149330..a5dbf5d0 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -297,8 +297,9 @@ class DecimalField(BaseField): if value is None: return value - return decimal.Decimal(value).quantize(self.precision, - rounding=self.rounding) + # Convert to string for python 2.6 before casting to Decimal + value = decimal.Decimal("%s" % value) + return value.quantize(self.precision, rounding=self.rounding) def to_mongo(self, value): if value is None: From 7765f272ac74f1b25e9e057d13b54408031d1594 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 26 Apr 2013 08:46:46 +0000 Subject: [PATCH 02/40] Documentation api and reference cleanups --- docs/django.rst | 2 +- docs/guide/defining-documents.rst | 82 +++++++++++++++---------------- docs/guide/document-instances.rst | 4 +- docs/guide/gridfs.rst | 6 +-- docs/guide/querying.rst | 16 +++--- docs/tutorial.rst | 8 +-- docs/upgrade.rst | 11 ++++- mongoengine/document.py | 2 +- mongoengine/fields.py | 2 +- mongoengine/queryset/queryset.py | 8 +-- 10 files changed, 75 insertions(+), 66 deletions(-) diff --git a/docs/django.rst b/docs/django.rst index e3a1c6b1..d60e55d9 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -98,7 +98,7 @@ Django provides session cookie, which expires after ```SESSION_COOKIE_AGE``` sec Storage ======= -With MongoEngine's support for GridFS via the :class:`~mongoengine.FileField`, +With MongoEngine's support for GridFS via the :class:`~mongoengine.fields.FileField`, it is useful to have a Django file storage backend that wraps this. The new storage module is called :class:`~mongoengine.django.storage.GridFSStorage`. Using it is very similar to using the default FileSystemStorage.:: diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 350ba678..d18606ac 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -62,31 +62,31 @@ not provided. Default values may optionally be a callable, which will be called to retrieve the value (such as in the above example). The field types available are as follows: -* :class:`~mongoengine.BinaryField` -* :class:`~mongoengine.BooleanField` -* :class:`~mongoengine.ComplexDateTimeField` -* :class:`~mongoengine.DateTimeField` -* :class:`~mongoengine.DecimalField` -* :class:`~mongoengine.DictField` -* :class:`~mongoengine.DynamicField` -* :class:`~mongoengine.EmailField` -* :class:`~mongoengine.EmbeddedDocumentField` -* :class:`~mongoengine.FileField` -* :class:`~mongoengine.FloatField` -* :class:`~mongoengine.GenericEmbeddedDocumentField` -* :class:`~mongoengine.GenericReferenceField` -* :class:`~mongoengine.GeoPointField` -* :class:`~mongoengine.ImageField` -* :class:`~mongoengine.IntField` -* :class:`~mongoengine.ListField` -* :class:`~mongoengine.MapField` -* :class:`~mongoengine.ObjectIdField` -* :class:`~mongoengine.ReferenceField` -* :class:`~mongoengine.SequenceField` -* :class:`~mongoengine.SortedListField` -* :class:`~mongoengine.StringField` -* :class:`~mongoengine.URLField` -* :class:`~mongoengine.UUIDField` +* :class:`~mongoengine.fields.BinaryField` +* :class:`~mongoengine.fields.BooleanField` +* :class:`~mongoengine.fields.ComplexDateTimeField` +* :class:`~mongoengine.fields.DateTimeField` +* :class:`~mongoengine.fields.DecimalField` +* :class:`~mongoengine.fields.DictField` +* :class:`~mongoengine.fields.DynamicField` +* :class:`~mongoengine.fields.EmailField` +* :class:`~mongoengine.fields.EmbeddedDocumentField` +* :class:`~mongoengine.fields.FileField` +* :class:`~mongoengine.fields.FloatField` +* :class:`~mongoengine.fields.GenericEmbeddedDocumentField` +* :class:`~mongoengine.fields.GenericReferenceField` +* :class:`~mongoengine.fields.GeoPointField` +* :class:`~mongoengine.fields.ImageField` +* :class:`~mongoengine.fields.IntField` +* :class:`~mongoengine.fields.ListField` +* :class:`~mongoengine.fields.MapField` +* :class:`~mongoengine.fields.ObjectIdField` +* :class:`~mongoengine.fields.ReferenceField` +* :class:`~mongoengine.fields.SequenceField` +* :class:`~mongoengine.fields.SortedListField` +* :class:`~mongoengine.fields.StringField` +* :class:`~mongoengine.fields.URLField` +* :class:`~mongoengine.fields.UUIDField` Field arguments --------------- @@ -110,7 +110,7 @@ arguments can be set on all fields: The definion of default parameters follow `the general rules on Python `__, which means that some care should be taken when dealing with default mutable objects - (like in :class:`~mongoengine.ListField` or :class:`~mongoengine.DictField`):: + (like in :class:`~mongoengine.fields.ListField` or :class:`~mongoengine.fields.DictField`):: class ExampleFirst(Document): # Default an empty list @@ -172,8 +172,8 @@ arguments can be set on all fields: List fields ----------- MongoDB allows the storage of lists of items. To add a list of items to a -:class:`~mongoengine.Document`, use the :class:`~mongoengine.ListField` field -type. :class:`~mongoengine.ListField` takes another field object as its first +:class:`~mongoengine.Document`, use the :class:`~mongoengine.fields.ListField` field +type. :class:`~mongoengine.fields.ListField` takes another field object as its first argument, which specifies which type elements may be stored within the list:: class Page(Document): @@ -191,7 +191,7 @@ inherit from :class:`~mongoengine.EmbeddedDocument` rather than content = StringField() To embed the document within another document, use the -:class:`~mongoengine.EmbeddedDocumentField` field type, providing the embedded +:class:`~mongoengine.fields.EmbeddedDocumentField` field type, providing the embedded document class as the first argument:: class Page(Document): @@ -206,7 +206,7 @@ Dictionary Fields Often, an embedded document may be used instead of a dictionary -- generally this is recommended as dictionaries don't support validation or custom field types. However, sometimes you will not know the structure of what you want to -store; in this situation a :class:`~mongoengine.DictField` is appropriate:: +store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate:: class SurveyResponse(Document): date = DateTimeField() @@ -224,7 +224,7 @@ other objects, so are the most flexible field type available. Reference fields ---------------- References may be stored to other documents in the database using the -:class:`~mongoengine.ReferenceField`. Pass in another document class as the +:class:`~mongoengine.fields.ReferenceField`. Pass in another document class as the first argument to the constructor, then simply assign document objects to the field:: @@ -245,9 +245,9 @@ field:: The :class:`User` object is automatically turned into a reference behind the scenes, and dereferenced when the :class:`Page` object is retrieved. -To add a :class:`~mongoengine.ReferenceField` that references the document +To add a :class:`~mongoengine.fields.ReferenceField` that references the document being defined, use the string ``'self'`` in place of the document class as the -argument to :class:`~mongoengine.ReferenceField`'s constructor. To reference a +argument to :class:`~mongoengine.fields.ReferenceField`'s constructor. To reference a document that has not yet been defined, use the name of the undefined document as the constructor's argument:: @@ -325,7 +325,7 @@ Its value can take any of the following constants: :const:`mongoengine.PULL` Removes the reference to the object (using MongoDB's "pull" operation) from any object's fields of - :class:`~mongoengine.ListField` (:class:`~mongoengine.ReferenceField`). + :class:`~mongoengine.fields.ListField` (:class:`~mongoengine.fields.ReferenceField`). .. warning:: @@ -352,7 +352,7 @@ Its value can take any of the following constants: Generic reference fields '''''''''''''''''''''''' A second kind of reference field also exists, -:class:`~mongoengine.GenericReferenceField`. This allows you to reference any +:class:`~mongoengine.fields.GenericReferenceField`. This allows you to reference any kind of :class:`~mongoengine.Document`, and hence doesn't take a :class:`~mongoengine.Document` subclass as a constructor argument:: @@ -376,15 +376,15 @@ kind of :class:`~mongoengine.Document`, and hence doesn't take a .. note:: - Using :class:`~mongoengine.GenericReferenceField`\ s is slightly less - efficient than the standard :class:`~mongoengine.ReferenceField`\ s, so if + Using :class:`~mongoengine.fields.GenericReferenceField`\ s is slightly less + efficient than the standard :class:`~mongoengine.fields.ReferenceField`\ s, so if you will only be referencing one document type, prefer the standard - :class:`~mongoengine.ReferenceField`. + :class:`~mongoengine.fields.ReferenceField`. Uniqueness constraints ---------------------- MongoEngine allows you to specify that a field should be unique across a -collection by providing ``unique=True`` to a :class:`~mongoengine.Field`\ 's +collection by providing ``unique=True`` to a :class:`~mongoengine.fields.Field`\ 's constructor. If you try to save a document that has the same value for a unique field as a document that is already in the database, a :class:`~mongoengine.OperationError` will be raised. You may also specify @@ -492,11 +492,11 @@ Geospatial indexes ------------------ Geospatial indexes will be automatically created for all -:class:`~mongoengine.GeoPointField`\ s +:class:`~mongoengine.fields.GeoPointField`\ s It is also possible to explicitly define geospatial indexes. This is useful if you need to define a geospatial index on a subfield of a -:class:`~mongoengine.DictField` or a custom field that contains a +:class:`~mongoengine.fields.DictField` or a custom field that contains a point. To create a geospatial index you must prefix the field with the ***** sign. :: diff --git a/docs/guide/document-instances.rst b/docs/guide/document-instances.rst index e8e7d63c..619f3e83 100644 --- a/docs/guide/document-instances.rst +++ b/docs/guide/document-instances.rst @@ -68,8 +68,8 @@ document values for example:: Cascading Saves --------------- -If your document contains :class:`~mongoengine.ReferenceField` or -:class:`~mongoengine.GenericReferenceField` objects, then by default the +If your document contains :class:`~mongoengine.fields.ReferenceField` or +:class:`~mongoengine.fields.GenericReferenceField` objects, then by default the :meth:`~mongoengine.Document.save` method will automatically save any changes to those objects as well. If this is not desired passing :attr:`cascade` as False to the save method turns this feature off. diff --git a/docs/guide/gridfs.rst b/docs/guide/gridfs.rst index 11259477..d81bb922 100644 --- a/docs/guide/gridfs.rst +++ b/docs/guide/gridfs.rst @@ -7,7 +7,7 @@ GridFS Writing ------- -GridFS support comes in the form of the :class:`~mongoengine.FileField` field +GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field object. This field acts as a file-like object and provides a couple of different ways of inserting and retrieving data. Arbitrary metadata such as content type can also be stored alongside the files. In the following example, @@ -27,7 +27,7 @@ a document is created to store details about animals, including a photo:: Retrieval --------- -So using the :class:`~mongoengine.FileField` is just like using any other +So using the :class:`~mongoengine.fields.FileField` is just like using any other field. The file can also be retrieved just as easily:: marmot = Animal.objects(genus='Marmota').first() @@ -37,7 +37,7 @@ field. The file can also be retrieved just as easily:: Streaming --------- -Streaming data into a :class:`~mongoengine.FileField` is achieved in a +Streaming data into a :class:`~mongoengine.fields.FileField` is achieved in a slightly different manner. First, a new file must be created by calling the :func:`new_file` method. Data can then be written using :func:`write`:: diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 60702ec6..3a25c286 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -79,7 +79,7 @@ expressions: * ``match`` -- performs an $elemMatch so you can match an entire document within an array There are a few special operators for performing geographical queries, that -may used with :class:`~mongoengine.GeoPointField`\ s: +may used with :class:`~mongoengine.fields.GeoPointField`\ s: * ``within_distance`` -- provide a list containing a point and a maximum distance (e.g. [(41.342, -87.653), 5]) @@ -100,7 +100,7 @@ Querying lists -------------- On most fields, this syntax will look up documents where the field specified matches the given value exactly, but when the field refers to a -:class:`~mongoengine.ListField`, a single item may be provided, in which case +:class:`~mongoengine.fields.ListField`, a single item may be provided, in which case lists that contain that item will be matched:: class Page(Document): @@ -319,7 +319,7 @@ Retrieving a subset of fields Sometimes a subset of fields on a :class:`~mongoengine.Document` is required, and for efficiency only these should be retrieved from the database. This issue is especially important for MongoDB, as fields may often be extremely large -(e.g. a :class:`~mongoengine.ListField` of +(e.g. a :class:`~mongoengine.fields.ListField` of :class:`~mongoengine.EmbeddedDocument`\ s, which represent the comments on a blog post. To select only a subset of fields, use :meth:`~mongoengine.queryset.QuerySet.only`, specifying the fields you want to @@ -351,14 +351,14 @@ If you later need the missing fields, just call Getting related data -------------------- -When iterating the results of :class:`~mongoengine.ListField` or -:class:`~mongoengine.DictField` we automatically dereference any +When iterating the results of :class:`~mongoengine.fields.ListField` or +:class:`~mongoengine.fields.DictField` we automatically dereference any :class:`~pymongo.dbref.DBRef` objects as efficiently as possible, reducing the number the queries to mongo. There are times when that efficiency is not enough, documents that have -:class:`~mongoengine.ReferenceField` objects or -:class:`~mongoengine.GenericReferenceField` objects at the top level are +:class:`~mongoengine.fields.ReferenceField` objects or +:class:`~mongoengine.fields.GenericReferenceField` objects at the top level are expensive as the number of queries to MongoDB can quickly rise. To limit the number of queries use @@ -541,7 +541,7 @@ Javascript code. When accessing a field on a collection object, use square-bracket notation, and prefix the MongoEngine field name with a tilde. The field name that follows the tilde will be translated to the name used in the database. Note that when referring to fields on embedded documents, -the name of the :class:`~mongoengine.EmbeddedDocumentField`, followed by a dot, +the name of the :class:`~mongoengine.fields.EmbeddedDocumentField`, followed by a dot, should be used before the name of the field on the embedded document. The following example shows how the substitutions are made:: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 423df9b0..c2f481b9 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -115,7 +115,7 @@ by setting :attr:`allow_inheritance` to True in the :attr:`meta`:: link_url = StringField() We are storing a reference to the author of the posts using a -:class:`~mongoengine.ReferenceField` object. These are similar to foreign key +:class:`~mongoengine.fields.ReferenceField` object. These are similar to foreign key fields in traditional ORMs, and are automatically translated into references when they are saved, and dereferenced when they are loaded. @@ -137,7 +137,7 @@ size of our database. So let's take a look that the code our modified author = ReferenceField(User) tags = ListField(StringField(max_length=30)) -The :class:`~mongoengine.ListField` object that is used to define a Post's tags +The :class:`~mongoengine.fields.ListField` object that is used to define a Post's tags takes a field object as its first argument --- this means that you can have lists of any type of field (including lists). @@ -174,7 +174,7 @@ We can then store a list of comment documents in our post document:: Handling deletions of references ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The :class:`~mongoengine.ReferenceField` object takes a keyword +The :class:`~mongoengine.fields.ReferenceField` object takes a keyword `reverse_delete_rule` for handling deletion rules if the reference is deleted. To delete all the posts if a user is deleted set the rule:: @@ -184,7 +184,7 @@ To delete all the posts if a user is deleted set the rule:: tags = ListField(StringField(max_length=30)) comments = ListField(EmbeddedDocumentField(Comment)) -See :class:`~mongoengine.ReferenceField` for more information. +See :class:`~mongoengine.fields.ReferenceField` for more information. .. note:: MapFields and DictFields currently don't support automatic handling of diff --git a/docs/upgrade.rst b/docs/upgrade.rst index dddce912..0ae65f32 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -120,6 +120,9 @@ eg:: p._mark_as_dirty('friends') p.save() +`An example test migration is available on github +`_. + UUIDField --------- @@ -145,6 +148,9 @@ eg:: a._mark_as_dirty('uuid') a.save() +`An example test migration is available on github +`_. + DecimalField ------------ @@ -172,7 +178,10 @@ eg:: p.save() .. note:: DecimalField's have also been improved with the addition of precision - and rounding. See :class:`~mongoengine.DecimalField` for more information. + and rounding. See :class:`~mongoengine.fields.DecimalField` for more information. + +`An example test migration is available on github +`_. Cascading Saves --------------- diff --git a/mongoengine/document.py b/mongoengine/document.py index d0cafa3e..c4542a2e 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -559,7 +559,7 @@ class DynamicDocument(Document): way as an ordinary document but has expando style properties. Any data passed or set against the :class:`~mongoengine.DynamicDocument` that is not a field is automatically converted into a - :class:`~mongoengine.DynamicField` and data can be attributed to that + :class:`~mongoengine.fields.DynamicField` and data can be attributed to that field. .. note:: diff --git a/mongoengine/fields.py b/mongoengine/fields.py index a5dbf5d0..cf2c802c 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -782,7 +782,7 @@ class ReferenceField(BaseField): * NULLIFY - Updates the reference to null. * CASCADE - Deletes the documents associated with the reference. * DENY - Prevent the deletion of the reference object. - * PULL - Pull the reference from a :class:`~mongoengine.ListField` + * PULL - Pull the reference from a :class:`~mongoengine.fields.ListField` of references Alternative syntax for registering delete rules (useful when implementing diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index dcfb2402..769cf68b 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1049,7 +1049,7 @@ class QuerySet(object): """) for result in self.map_reduce(map_func, reduce_func, - finalize_f=finalize_func, output='inline'): + finalize_f=finalize_func, output='inline'): return result.value else: return 0 @@ -1062,11 +1062,11 @@ class QuerySet(object): .. note:: Can only do direct simple mappings and cannot map across - :class:`~mongoengine.ReferenceField` or - :class:`~mongoengine.GenericReferenceField` for more complex + :class:`~mongoengine.fields.ReferenceField` or + :class:`~mongoengine.fields.GenericReferenceField` for more complex counting a manual map reduce call would is required. - If the field is a :class:`~mongoengine.ListField`, the items within + If the field is a :class:`~mongoengine.fields.ListField`, the items within each list will be counted individually. :param field: the field to use From 2447349383ca86dd57ff83403ce6bf9aebef90f6 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 26 Apr 2013 09:59:43 +0000 Subject: [PATCH 03/40] Added a note about distinct being a command --- mongoengine/queryset/queryset.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 769cf68b..5c7c7c89 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -608,6 +608,9 @@ class QuerySet(object): :param field: the field to select distinct values from + .. note:: This is a command and won't take ordering or limit into + account. + .. versionadded:: 0.4 .. versionchanged:: 0.5 - Fixed handling references .. versionchanged:: 0.6 - Improved db_field refrence handling From 36993097b4668e20e809a9bbb9a575b45b004939 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 26 Apr 2013 11:38:45 +0000 Subject: [PATCH 04/40] Document serialization uses field order to ensure a strict order is set (#296) --- docs/changelog.rst | 1 + docs/guide/defining-documents.rst | 4 ++++ docs/guide/document-instances.rst | 14 ++++++++----- docs/upgrade.rst | 19 ++++++++++++++--- mongoengine/base/document.py | 34 ++++++++++++++++++++++--------- tests/document/dynamic.py | 3 ++- tests/document/inheritance.py | 27 ++++++++++++++++++++---- tests/document/instance.py | 15 ++++++++++++++ 8 files changed, 94 insertions(+), 23 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d0167c51..f786c1d6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.X ================ +- Document serialization uses field order to ensure a strict order is set (#296) - DecimalField now stores as float not string (#289) - UUIDField now stores as a binary by default (#292) - Added Custom User Model for Django 1.5 (#285) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index d18606ac..36e0efea 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -24,6 +24,9 @@ objects** as class attributes to the document class:: title = StringField(max_length=200, required=True) date_modified = DateTimeField(default=datetime.datetime.now) +As BSON (the binary format for storing data in mongodb) is order dependent, +documents are serialized based on their field order. + Dynamic document schemas ======================== One of the benefits of MongoDb is dynamic schemas for a collection, whilst data @@ -51,6 +54,7 @@ be saved :: There is one caveat on Dynamic Documents: fields cannot start with `_` +Dynamic fields are stored in alphabetical order *after* any declared fields. Fields ====== diff --git a/docs/guide/document-instances.rst b/docs/guide/document-instances.rst index 619f3e83..f9a6610f 100644 --- a/docs/guide/document-instances.rst +++ b/docs/guide/document-instances.rst @@ -30,11 +30,14 @@ already exist, then any changes will be updated atomically. For example:: .. note:: - Changes to documents are tracked and on the whole perform `set` operations. + Changes to documents are tracked and on the whole perform ``set`` operations. - * ``list_field.pop(0)`` - *sets* the resulting list + * ``list_field.push(0)`` - *sets* the resulting list * ``del(list_field)`` - *unsets* whole list + With lists its preferable to use ``Doc.update(push__list_field=0)`` as + this stops the whole list being updated - stopping any race conditions. + .. seealso:: :ref:`guide-atomic-updates` @@ -70,9 +73,10 @@ Cascading Saves --------------- If your document contains :class:`~mongoengine.fields.ReferenceField` or :class:`~mongoengine.fields.GenericReferenceField` objects, then by default the -:meth:`~mongoengine.Document.save` method will automatically save any changes to -those objects as well. If this is not desired passing :attr:`cascade` as False -to the save method turns this feature off. +:meth:`~mongoengine.Document.save` method will not save any changes to +those objects. If you want all references to also be saved also, noting each +save is a separate query, then passing :attr:`cascade` as True +to the save method will cascade any saves. Deleting documents ------------------ diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 0ae65f32..bb5705ca 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -120,7 +120,7 @@ eg:: p._mark_as_dirty('friends') p.save() -`An example test migration is available on github +`An example test migration for ReferenceFields is available on github `_. UUIDField @@ -148,7 +148,7 @@ eg:: a._mark_as_dirty('uuid') a.save() -`An example test migration is available on github +`An example test migration for UUIDFields is available on github `_. DecimalField @@ -180,7 +180,7 @@ eg:: .. note:: DecimalField's have also been improved with the addition of precision and rounding. See :class:`~mongoengine.fields.DecimalField` for more information. -`An example test migration is available on github +`An example test migration for DecimalFields is available on github `_. Cascading Saves @@ -196,6 +196,19 @@ you will have to explicitly tell it to cascade on save:: # Or on save: my_document.save(cascade=True) +Storage +------- + +Document and Embedded Documents are now serialized based on declared field order. +Previously, the data was passed to mongodb as a dictionary and which meant that +order wasn't guaranteed - so things like ``$addToSet`` operations on +:class:`~mongoengine.EmbeddedDocument` could potentially fail in unexpected +ways. + +If this impacts you, you may want to rewrite the objects using the +``doc.mark_as_dirty('field')`` pattern described above. If you are using a +compound primary key then you will need to ensure the order is fixed and match +your EmbeddedDocument to that order. Querysets ========= diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 7ec672fe..53686b25 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -6,6 +6,7 @@ from functools import partial import pymongo from bson import json_util from bson.dbref import DBRef +from bson.son import SON from mongoengine import signals from mongoengine.common import _import_class @@ -228,11 +229,16 @@ class BaseDocument(object): pass def to_mongo(self): - """Return data dictionary ready for use with MongoDB. + """Return as SON data ready for use with MongoDB. """ - data = {} - for field_name, field in self._fields.iteritems(): + data = SON() + data["_id"] = None + data['_cls'] = self._class_name + + for field_name in self: value = self._data.get(field_name, None) + field = self._fields.get(field_name) + if value is not None: value = field.to_mongo(value) @@ -244,19 +250,27 @@ class BaseDocument(object): if value is not None: data[field.db_field] = value - # Only add _cls if allow_inheritance is True - if (hasattr(self, '_meta') and - self._meta.get('allow_inheritance', ALLOW_INHERITANCE) == True): - data['_cls'] = self._class_name + # If "_id" has not been set, then try and set it + if data["_id"] is None: + data["_id"] = self._data.get("id", None) - if '_id' in data and data['_id'] is None: - del data['_id'] + if data['_id'] is None: + data.pop('_id') + + # Only add _cls if allow_inheritance is True + if (not hasattr(self, '_meta') or + not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)): + data.pop('_cls') if not self._dynamic: return data - for name, field in self._dynamic_fields.items(): + # Sort dynamic fields by key + dynamic_fields = sorted(self._dynamic_fields.iteritems(), + key=operator.itemgetter(0)) + for name, field in dynamic_fields: data[name] = field.to_mongo(self._data.get(name, None)) + return data def validate(self, clean=True): diff --git a/tests/document/dynamic.py b/tests/document/dynamic.py index 5881cd07..6263e68c 100644 --- a/tests/document/dynamic.py +++ b/tests/document/dynamic.py @@ -31,8 +31,9 @@ class DynamicTest(unittest.TestCase): self.assertEqual(p.to_mongo(), {"_cls": "Person", "name": "James", "age": 34}) - + self.assertEqual(p.to_mongo().keys(), ["_cls", "name", "age"]) p.save() + self.assertEqual(p.to_mongo().keys(), ["_id", "_cls", "name", "age"]) self.assertEqual(self.Person.objects.first().age, 34) diff --git a/tests/document/inheritance.py b/tests/document/inheritance.py index 3b550f1a..f0116311 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -143,7 +143,7 @@ class InheritanceTest(unittest.TestCase): self.assertEqual(Animal._superclasses, ()) self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish', - 'Animal.Fish.Pike')) + 'Animal.Fish.Pike')) self.assertEqual(Fish._superclasses, ('Animal', )) self.assertEqual(Fish._subclasses, ('Animal.Fish', 'Animal.Fish.Pike')) @@ -168,6 +168,26 @@ class InheritanceTest(unittest.TestCase): self.assertEqual(Employee._get_collection_name(), Person._get_collection_name()) + def test_inheritance_to_mongo_keys(self): + """Ensure that document may inherit fields from a superclass document. + """ + class Person(Document): + name = StringField() + age = IntField() + + meta = {'allow_inheritance': True} + + class Employee(Person): + salary = IntField() + + self.assertEqual(['age', 'id', 'name', 'salary'], + sorted(Employee._fields.keys())) + self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(), + ['_cls', 'name', 'age']) + self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(), + ['_cls', 'name', 'age', 'salary']) + self.assertEqual(Employee._get_collection_name(), + Person._get_collection_name()) def test_polymorphic_queries(self): """Ensure that the correct subclasses are returned from a query @@ -197,7 +217,6 @@ class InheritanceTest(unittest.TestCase): classes = [obj.__class__ for obj in Human.objects] self.assertEqual(classes, [Human]) - def test_allow_inheritance(self): """Ensure that inheritance may be disabled on simple classes and that _cls and _subclasses will not be used. @@ -213,8 +232,8 @@ class InheritanceTest(unittest.TestCase): self.assertRaises(ValueError, create_dog_class) # Check that _cls etc aren't present on simple documents - dog = Animal(name='dog') - dog.save() + dog = Animal(name='dog').save() + self.assertEqual(dog.to_mongo().keys(), ['_id', 'name']) collection = self.db[Animal._get_collection_name()] obj = collection.find_one() diff --git a/tests/document/instance.py b/tests/document/instance.py index b800d90f..06744ab4 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -428,6 +428,21 @@ class InstanceTest(unittest.TestCase): self.assertFalse('age' in person) self.assertFalse('nationality' in person) + def test_embedded_document_to_mongo(self): + class Person(EmbeddedDocument): + name = StringField() + age = IntField() + + meta = {"allow_inheritance": True} + + class Employee(Person): + salary = IntField() + + self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(), + ['_cls', 'name', 'age']) + self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(), + ['_cls', 'name', 'age', 'salary']) + def test_embedded_document(self): """Ensure that embedded documents are set up correctly. """ From 5e65d278324c86bc615747108658957b4e9dff03 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 26 Apr 2013 11:46:12 +0000 Subject: [PATCH 05/40] PEP8 x == True should be x is True --- mongoengine/base/fields.py | 2 +- mongoengine/document.py | 2 +- mongoengine/queryset/queryset.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 6ebba362..3929a3a5 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -295,7 +295,7 @@ class ComplexBaseField(BaseField): meta = getattr(v, '_meta', {}) allow_inheritance = ( meta.get('allow_inheritance', ALLOW_INHERITANCE) - == True) + is True) if not allow_inheritance and not self.field: value_dict[k] = GenericReferenceField().to_mongo(v) else: diff --git a/mongoengine/document.py b/mongoengine/document.py index c4542a2e..bd6ce191 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -548,7 +548,7 @@ class Document(BaseDocument): # If _cls is being used (for polymorphism), it needs an index, # only if another index doesn't begin with _cls if (index_cls and not cls_indexed and - cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) == True): + cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True): collection.ensure_index('_cls', background=background, **index_opts) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 5c7c7c89..65d6553f 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -66,7 +66,7 @@ class QuerySet(object): # If inheritance is allowed, only return instances and instances of # subclasses of the class being used - if document._meta.get('allow_inheritance') == True: + if document._meta.get('allow_inheritance') is True: self._initial_query = {"_cls": {"$in": self._document._subclasses}} self._loaded_fields = QueryFieldList(always_include=['_cls']) self._cursor_obj = None From 6e2d2f33deeaee2b69685a9c2389cacdfefefa38 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 26 Apr 2013 14:33:40 +0000 Subject: [PATCH 06/40] Updated benchmarks for #27 --- benchmark.py | 143 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/benchmark.py b/benchmark.py index 0197e1d7..16b2fd47 100644 --- a/benchmark.py +++ b/benchmark.py @@ -86,17 +86,43 @@ def main(): ---------------------------------------------------------------------------------------------------- Creating 10000 dictionaries - MongoEngine, force=True 8.36906409264 + 0.8.X + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries - Pymongo + 3.69964408875 + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries - Pymongo write_concern={"w": 0} + 3.5526599884 + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries - MongoEngine + 7.00959801674 + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries without continual assign - MongoEngine + 5.60943293571 + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade=True + 6.715102911 + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True + 5.50644683838 + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False + 4.69851183891 + ---------------------------------------------------------------------------------------------------- + Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False + 4.68946313858 + ---------------------------------------------------------------------------------------------------- """ setup = """ -from pymongo import Connection -connection = Connection() +from pymongo import MongoClient +connection = MongoClient() connection.drop_database('timeit_test') """ stmt = """ -from pymongo import Connection -connection = Connection() +from pymongo import MongoClient +connection = MongoClient() db = connection.timeit_test noddy = db.noddy @@ -106,7 +132,7 @@ for i in xrange(10000): for j in range(20): example['fields']["key"+str(j)] = "value "+str(j) - noddy.insert(example) + noddy.save(example) myNoddys = noddy.find() [n for n in myNoddys] # iterate @@ -117,9 +143,32 @@ myNoddys = noddy.find() t = timeit.Timer(stmt=stmt, setup=setup) print t.timeit(1) + stmt = """ +from pymongo import MongoClient +connection = MongoClient() + +db = connection.timeit_test +noddy = db.noddy + +for i in xrange(10000): + example = {'fields': {}} + for j in range(20): + example['fields']["key"+str(j)] = "value "+str(j) + + noddy.save(example, write_concern={"w": 0}) + +myNoddys = noddy.find() +[n for n in myNoddys] # iterate +""" + + print "-" * 100 + print """Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""" + t = timeit.Timer(stmt=stmt, setup=setup) + print t.timeit(1) + setup = """ -from pymongo import Connection -connection = Connection() +from pymongo import MongoClient +connection = MongoClient() connection.drop_database('timeit_test') connection.disconnect() @@ -149,33 +198,18 @@ myNoddys = Noddy.objects() stmt = """ for i in xrange(10000): noddy = Noddy() + fields = {} for j in range(20): - noddy.fields["key"+str(j)] = "value "+str(j) - noddy.save(safe=False, validate=False) + fields["key"+str(j)] = "value "+str(j) + noddy.fields = fields + noddy.save() myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ print "-" * 100 - print """Creating 10000 dictionaries - MongoEngine, safe=False, validate=False""" - t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) - - - stmt = """ -for i in xrange(10000): - noddy = Noddy() - for j in range(20): - noddy.fields["key"+str(j)] = "value "+str(j) - noddy.save(safe=False, validate=False, cascade=False) - -myNoddys = Noddy.objects() -[n for n in myNoddys] # iterate -""" - - print "-" * 100 - print """Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False""" + print """Creating 10000 dictionaries without continual assign - MongoEngine""" t = timeit.Timer(stmt=stmt, setup=setup) print t.timeit(1) @@ -184,16 +218,65 @@ for i in xrange(10000): noddy = Noddy() for j in range(20): noddy.fields["key"+str(j)] = "value "+str(j) - noddy.save(force_insert=True, safe=False, validate=False, cascade=False) + noddy.save(write_concern={"w": 0}, cascade=True) myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ print "-" * 100 - print """Creating 10000 dictionaries - MongoEngine, force=True""" + print """Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""" t = timeit.Timer(stmt=stmt, setup=setup) print t.timeit(1) + stmt = """ +for i in xrange(10000): + noddy = Noddy() + for j in range(20): + noddy.fields["key"+str(j)] = "value "+str(j) + noddy.save(write_concern={"w": 0}, validate=False, cascade=True) + +myNoddys = Noddy.objects() +[n for n in myNoddys] # iterate +""" + + print "-" * 100 + print """Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""" + t = timeit.Timer(stmt=stmt, setup=setup) + print t.timeit(1) + + stmt = """ +for i in xrange(10000): + noddy = Noddy() + for j in range(20): + noddy.fields["key"+str(j)] = "value "+str(j) + noddy.save(validate=False, write_concern={"w": 0}) + +myNoddys = Noddy.objects() +[n for n in myNoddys] # iterate +""" + + print "-" * 100 + print """Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False""" + t = timeit.Timer(stmt=stmt, setup=setup) + print t.timeit(1) + + stmt = """ +for i in xrange(10000): + noddy = Noddy() + for j in range(20): + noddy.fields["key"+str(j)] = "value "+str(j) + noddy.save(force_insert=True, write_concern={"w": 0}, validate=False) + +myNoddys = Noddy.objects() +[n for n in myNoddys] # iterate +""" + + print "-" * 100 + print """Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False""" + t = timeit.Timer(stmt=stmt, setup=setup) + print t.timeit(1) + + if __name__ == "__main__": - main() + main() \ No newline at end of file From b0c1ec04b5f39be0f08d2765e1070c7ed0652d60 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 29 Apr 2013 07:38:31 +0000 Subject: [PATCH 07/40] Improvements to indexing documentation (#130) --- docs/guide/defining-documents.rst | 20 ++++++++++++++++++++ mongoengine/fields.py | 6 +++--- tests/document/indexes.py | 11 +++++------ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 36e0efea..c404101f 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -479,6 +479,10 @@ If a dictionary is passed then the following options are available: :attr:`unique` (Default: False) Whether the index should be unique. +:attr:`expireAfterSeconds` (Optional) + Allows you to automatically expire data from a collection by setting the + time in seconds to expire the a field. + .. note:: Inheritance adds extra fields indices see: :ref:`document-inheritance`. @@ -512,6 +516,22 @@ point. To create a geospatial index you must prefix the field with the ], } +Time To Live indexes +-------------------- + +A special index type that allows you to automatically expire data from a +collection after a given period. See the official +`ttl `_ +documentation for more information. A common usecase might be session data:: + + class Session(Document): + created = DateTimeField(default=datetime.now) + meta = { + 'indexes': [ + {'fields': ['created'], 'expireAfterSeconds': 3600} + ] + } + Ordering ======== A default ordering can be specified for your diff --git a/mongoengine/fields.py b/mongoengine/fields.py index cf2c802c..bb2539cc 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -8,6 +8,7 @@ import uuid import warnings from operator import itemgetter +import pymongo import gridfs from bson import Binary, DBRef, SON, ObjectId @@ -37,7 +38,6 @@ __all__ = ['StringField', 'URLField', 'EmailField', 'IntField', 'LongField', 'SequenceField', 'UUIDField'] - RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -1392,7 +1392,7 @@ class GeoPointField(BaseField): .. versionadded:: 0.4 """ - _geo_index = True + _geo_index = pymongo.GEO2D def validate(self, value): """Make sure that a geo-value is of type (x, y) @@ -1404,7 +1404,7 @@ class GeoPointField(BaseField): if not len(value) == 2: self.error('Value must be a two-dimensional point') if (not isinstance(value[0], (float, int)) and - not isinstance(value[1], (float, int))): + not isinstance(value[1], (float, int))): self.error('Both values in point must be float or int') diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 61e3c0e7..99aeca6d 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -217,7 +217,7 @@ class IndexesTest(unittest.TestCase): } self.assertEqual([{'fields': [('location.point', '2d')]}], - Place._meta['index_specs']) + Place._meta['index_specs']) Place.ensure_indexes() info = Place._get_collection().index_information() @@ -231,8 +231,7 @@ class IndexesTest(unittest.TestCase): location = DictField() class Place(Document): - current = DictField( - field=EmbeddedDocumentField('EmbeddedLocation')) + current = DictField(field=EmbeddedDocumentField('EmbeddedLocation')) meta = { 'allow_inheritance': True, 'indexes': [ @@ -241,7 +240,7 @@ class IndexesTest(unittest.TestCase): } self.assertEqual([{'fields': [('current.location.point', '2d')]}], - Place._meta['index_specs']) + Place._meta['index_specs']) Place.ensure_indexes() info = Place._get_collection().index_information() @@ -264,7 +263,7 @@ class IndexesTest(unittest.TestCase): self.assertEqual([{'fields': [('addDate', -1)], 'unique': True, 'sparse': True}], - BlogPost._meta['index_specs']) + BlogPost._meta['index_specs']) BlogPost.drop_collection() @@ -633,7 +632,7 @@ class IndexesTest(unittest.TestCase): list(Log.objects) info = Log.objects._collection.index_information() self.assertEqual(3600, - info['created_1']['expireAfterSeconds']) + info['created_1']['expireAfterSeconds']) def test_unique_and_indexes(self): """Ensure that 'unique' constraints aren't overridden by From 5d7444c115c043ed0a262f03193fd2f99dd55f1d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 29 Apr 2013 09:38:21 +0000 Subject: [PATCH 08/40] Ensure as_pymongo() and to_json honour only() and exclude() (#293) --- docs/changelog.rst | 1 + mongoengine/queryset/queryset.py | 15 +++++++++++---- tests/queryset/queryset.py | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f786c1d6..699c5a78 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.X ================ +- Ensure as_pymongo() and to_json honour only() and exclude() (#293) - Document serialization uses field order to ensure a strict order is set (#296) - DecimalField now stores as float not string (#289) - UUIDField now stores as a binary by default (#292) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 65d6553f..5ae889c5 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -822,8 +822,7 @@ class QuerySet(object): def to_json(self): """Converts a queryset to JSON""" - queryset = self.clone() - return json_util.dumps(queryset._collection_obj.find(queryset._query)) + return json_util.dumps(self.as_pymongo()) def from_json(self, json_data): """Converts json data to unsaved objects""" @@ -1095,7 +1094,7 @@ class QuerySet(object): raise StopIteration if self._scalar: return self._get_scalar(self._document._from_son( - self._cursor.next())) + self._cursor.next())) if self._as_pymongo: return self._get_as_pymongo(self._cursor.next()) @@ -1370,7 +1369,15 @@ class QuerySet(object): new_data = {} for key, value in data.iteritems(): new_path = '%s.%s' % (path, key) if path else key - if all_fields or new_path in self.__as_pymongo_fields: + + if all_fields: + include_field = True + elif self._loaded_fields.value == QueryFieldList.ONLY: + include_field = new_path in self.__as_pymongo_fields + else: + include_field = new_path not in self.__as_pymongo_fields + + if include_field: new_data[key] = clean(value, path=new_path) data = new_data elif isinstance(data, list): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 5e403c4e..5bf81835 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3276,6 +3276,28 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(results[1]['name'], 'Barack Obama') self.assertEqual(results[1]['price'], Decimal('2.22')) + def test_as_pymongo_json_limit_fields(self): + + class User(Document): + email = EmailField(unique=True, required=True) + password_hash = StringField(db_field='password_hash', required=True) + password_salt = StringField(db_field='password_salt', required=True) + + User.drop_collection() + User(email="ross@example.com", password_salt="SomeSalt", password_hash="SomeHash").save() + + serialized_user = User.objects.exclude('password_salt', 'password_hash').as_pymongo()[0] + self.assertEqual(set(['_id', 'email']), set(serialized_user.keys())) + + serialized_user = User.objects.exclude('id', 'password_salt', 'password_hash').to_json() + self.assertEqual('[{"email": "ross@example.com"}]', serialized_user) + + serialized_user = User.objects.exclude('password_salt').only('email').as_pymongo()[0] + self.assertEqual(set(['email']), set(serialized_user.keys())) + + serialized_user = User.objects.exclude('password_salt').only('email').to_json() + self.assertEqual('[{"email": "ross@example.com"}]', serialized_user) + def test_no_dereference(self): class Organization(Document): From 85b81fb12a3e6fd4a1129602c433ce381d45e925 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 29 Apr 2013 10:36:11 +0000 Subject: [PATCH 09/40] If values cant be compared mark as changed (#287) --- docs/changelog.rst | 1 + mongoengine/base/fields.py | 17 ++++++++++------- tests/fields/fields.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 699c5a78..ffe94d15 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.X ================ +- If values cant be compared mark as changed (#287) - Ensure as_pymongo() and to_json honour only() and exclude() (#293) - Document serialization uses field order to ensure a strict order is set (#296) - DecimalField now stores as float not string (#289) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 3929a3a5..d9ed2788 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -81,13 +81,16 @@ class BaseField(object): def __set__(self, instance, value): """Descriptor for assigning a value to a field in a document. """ - changed = False - if (self.name not in instance._data or - instance._data[self.name] != value): - changed = True - instance._data[self.name] = value - if changed and instance._initialised: - instance._mark_as_changed(self.name) + if instance._initialised: + try: + if (self.name not in instance._data or + instance._data[self.name] != value): + instance._mark_as_changed(self.name) + except: + # Values cant be compared eg: naive and tz datetimes + # So mark it as changed + instance._mark_as_changed(self.name) + instance._data[self.name] = value def error(self, message="", errors=None, field_name=None): """Raises a ValidationError. diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 4fa6989c..5474aa6f 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -409,6 +409,27 @@ class FieldTest(unittest.TestCase): log.time = '1pm' self.assertRaises(ValidationError, log.validate) + def test_datetime_tz_aware_mark_as_changed(self): + from mongoengine import connection + + # Reset the connections + connection._connection_settings = {} + connection._connections = {} + connection._dbs = {} + + connect(db='mongoenginetest', tz_aware=True) + + class LogEntry(Document): + time = DateTimeField() + + LogEntry.drop_collection() + + LogEntry(time=datetime.datetime(2013, 1, 1, 0, 0, 0)).save() + + log = LogEntry.objects.first() + log.time = datetime.datetime(2013, 1, 1, 0, 0, 0) + self.assertEqual(['time'], log._changed_fields) + def test_datetime(self): """Tests showing pymongo datetime fields handling of microseconds. Microseconds are rounded to the nearest millisecond and pre UTC From 9c1cd81adb4d240b9783ce80cef275858b96d5ca Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Apr 2013 14:46:23 +0000 Subject: [PATCH 10/40] Add support for new geojson fields, indexes and queries (#299) --- docs/apireference.rst | 5 +- docs/changelog.rst | 1 + docs/conf.py | 6 +- docs/django.rst | 8 +- docs/guide/defining-documents.rst | 29 +++ docs/guide/querying.rst | 72 ++++- docs/index.rst | 4 +- mongoengine/base/document.py | 24 +- mongoengine/base/fields.py | 112 +++++++- mongoengine/common.py | 3 +- mongoengine/document.py | 1 - mongoengine/fields.py | 108 ++++++-- mongoengine/queryset/queryset.py | 7 +- mongoengine/queryset/transform.py | 98 +++++-- tests/document/indexes.py | 28 +- tests/fields/__init__.py | 3 +- tests/fields/fields.py | 39 --- tests/fields/geo.py | 274 ++++++++++++++++++++ tests/queryset/__init__.py | 4 +- tests/queryset/geo.py | 418 ++++++++++++++++++++++++++++++ tests/queryset/queryset.py | 161 ------------ 21 files changed, 1101 insertions(+), 304 deletions(-) create mode 100644 tests/fields/geo.py create mode 100644 tests/queryset/geo.py diff --git a/docs/apireference.rst b/docs/apireference.rst index 3a156299..37370e20 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -76,10 +76,13 @@ Fields .. autoclass:: mongoengine.fields.BinaryField .. autoclass:: mongoengine.fields.FileField .. autoclass:: mongoengine.fields.ImageField -.. autoclass:: mongoengine.fields.GeoPointField .. autoclass:: mongoengine.fields.SequenceField .. autoclass:: mongoengine.fields.ObjectIdField .. autoclass:: mongoengine.fields.UUIDField +.. autoclass:: mongoengine.fields.GeoPointField +.. autoclass:: mongoengine.fields.PointField +.. autoclass:: mongoengine.fields.LineStringField +.. autoclass:: mongoengine.fields.PolygonField .. autoclass:: mongoengine.fields.GridFSError .. autoclass:: mongoengine.fields.GridFSProxy .. autoclass:: mongoengine.fields.ImageGridFsProxy diff --git a/docs/changelog.rst b/docs/changelog.rst index ffe94d15..207f0dd6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.X ================ +- Add support for new geojson fields, indexes and queries (#299) - If values cant be compared mark as changed (#287) - Ensure as_pymongo() and to_json honour only() and exclude() (#293) - Document serialization uses field order to ensure a strict order is set (#296) diff --git a/docs/conf.py b/docs/conf.py index 8bcb9ec9..40c1f430 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -132,7 +132,11 @@ html_theme_path = ['_themes'] html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +html_sidebars = { + 'index': ['globaltoc.html', 'searchbox.html'], + '**': ['localtoc.html', 'relations.html', 'searchbox.html'] +} + # Additional templates that should be rendered to pages, maps page names to # template names. diff --git a/docs/django.rst b/docs/django.rst index d60e55d9..09c91e7d 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -1,8 +1,8 @@ -============================= -Using MongoEngine with Django -============================= +============== +Django Support +============== -.. note:: Updated to support Django 1.4 +.. note:: Updated to support Django 1.5 Connecting ========== diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index c404101f..2c744b71 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -499,6 +499,35 @@ in this case use 'dot' notation to identify the value to index eg: `rank.title` Geospatial indexes ------------------ + +The best geo index for mongodb is the new "2dsphere", which has an improved +spherical model and provides better performance and more options when querying. +The following fields will explicitly add a "2dsphere" index: + + - :class:`~mongoengine.fields.PointField` + - :class:`~mongoengine.fields.LineStringField` + - :class:`~mongoengine.fields.PolygonField` + +As "2dsphere" indexes can be part of a compound index, you may not want the +automatic index but would prefer a compound index. In this example we turn off +auto indexing and explicitly declare a compound index on ``location`` and ``datetime``:: + + class Log(Document): + location = PointField(auto_index=False) + datetime = DateTimeField() + + meta = { + 'indexes': [[("location", "2dsphere"), ("datetime", 1)]] + } + + +Pre MongoDB 2.4 Geo +''''''''''''''''''' + +.. note:: For MongoDB < 2.4 this is still current, however the new 2dsphere + index is a big improvement over the previous 2D model - so upgrading is + advised. + Geospatial indexes will be automatically created for all :class:`~mongoengine.fields.GeoPointField`\ s diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 3a25c286..f1b6470f 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -65,6 +65,9 @@ Available operators are as follows: * ``size`` -- the size of the array is * ``exists`` -- value for field exists +String queries +-------------- + The following operators are available as shortcuts to querying with regular expressions: @@ -78,8 +81,71 @@ expressions: * ``iendswith`` -- string field ends with value (case insensitive) * ``match`` -- performs an $elemMatch so you can match an entire document within an array -There are a few special operators for performing geographical queries, that -may used with :class:`~mongoengine.fields.GeoPointField`\ s: + +Geo queries +----------- + +There are a few special operators for performing geographical queries. The following +were added in 0.8 for: :class:`~mongoengine.fields.PointField`, +:class:`~mongoengine.fields.LineStringField` and +:class:`~mongoengine.fields.PolygonField`: + +* ``geo_within`` -- Check if a geometry is within a polygon. For ease of use + it accepts either a geojson geometry or just the polygon coordinates eg:: + + loc.objects(point__geo_with=[[[40, 5], [40, 6], [41, 6], [40, 5]]]) + loc.objects(point__geo_with={"type": "Polygon", + "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) + +* ``geo_within_box`` - simplified geo_within searching with a box eg:: + + loc.objects(point__geo_within_box=[(-125.0, 35.0), (-100.0, 40.0)]) + loc.objects(point__geo_within_box=[, ]) + +* ``geo_within_polygon`` -- simplified geo_within searching within a simple polygon eg:: + + loc.objects(point__geo_within_polygon=[[40, 5], [40, 6], [41, 6], [40, 5]]) + loc.objects(point__geo_within_polygon=[ [ , ] , + [ , ] , + [ , ] ]) + +* ``geo_within_center`` -- simplified geo_within the flat circle radius of a point eg:: + + loc.objects(point__geo_within_center=[(-125.0, 35.0), 1]) + loc.objects(point__geo_within_center=[ [ , ] , ]) + +* ``geo_within_sphere`` -- simplified geo_within the spherical circle radius of a point eg:: + + loc.objects(point__geo_within_sphere=[(-125.0, 35.0), 1]) + loc.objects(point__geo_within_sphere=[ [ , ] , ]) + +* ``geo_intersects`` -- selects all locations that intersect with a geometry eg:: + + # Inferred from provided points lists: + loc.objects(poly__geo_intersects=[40, 6]) + loc.objects(poly__geo_intersects=[[40, 5], [40, 6]]) + loc.objects(poly__geo_intersects=[[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]) + + # With geoJson style objects + loc.objects(poly__geo_intersects={"type": "Point", "coordinates": [40, 6]}) + loc.objects(poly__geo_intersects={"type": "LineString", + "coordinates": [[40, 5], [40, 6]]}) + loc.objects(poly__geo_intersects={"type": "Polygon", + "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}) + +* ``near`` -- Find all the locations near a given point:: + + loc.objects(point__near=[40, 5]) + loc.objects(point__near={"type": "Point", "coordinates": [40, 5]}) + + + You can also set the maximum distance in meters as well:: + + loc.objects(point__near=[40, 5], point__max_distance=1000) + + +The older 2D indexes are still supported with the +:class:`~mongoengine.fields.GeoPointField`: * ``within_distance`` -- provide a list containing a point and a maximum distance (e.g. [(41.342, -87.653), 5]) @@ -91,7 +157,9 @@ may used with :class:`~mongoengine.fields.GeoPointField`\ s: [(35.0, -125.0), (40.0, -100.0)]) * ``within_polygon`` -- filter documents to those within a given polygon (e.g. [(41.91,-87.69), (41.92,-87.68), (41.91,-87.65), (41.89,-87.65)]). + .. note:: Requires Mongo Server 2.0 + * ``max_distance`` -- can be added to your location queries to set a maximum distance. diff --git a/docs/index.rst b/docs/index.rst index 4aca82da..6358a315 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -56,14 +56,16 @@ See the :doc:`changelog` for a full list of changes to MongoEngine and putting updates live in production **;)** .. toctree:: + :maxdepth: 1 + :numbered: :hidden: tutorial guide/index apireference - django changelog upgrade + django Indices and tables ------------------ diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 53686b25..c2ccc488 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -662,7 +662,8 @@ class BaseDocument(object): if include_cls and direction is not pymongo.GEO2D: index_list.insert(0, ('_cls', 1)) - spec['fields'] = index_list + if index_list: + spec['fields'] = index_list if spec.get('sparse', False) and len(spec['fields']) > 1: raise ValueError( 'Sparse indexes can only have one field in them. ' @@ -704,13 +705,13 @@ class BaseDocument(object): # Add the new index to the list fields = [("%s%s" % (namespace, f), pymongo.ASCENDING) - for f in unique_fields] + for f in unique_fields] index = {'fields': fields, 'unique': True, 'sparse': sparse} unique_indexes.append(index) # Grab any embedded document field unique indexes if (field.__class__.__name__ == "EmbeddedDocumentField" and - field.document_type != cls): + field.document_type != cls): field_namespace = "%s." % field_name doc_cls = field.document_type unique_indexes += doc_cls._unique_with_indexes(field_namespace) @@ -718,26 +719,31 @@ class BaseDocument(object): return unique_indexes @classmethod - def _geo_indices(cls, inspected=None): + def _geo_indices(cls, inspected=None, parent_field=None): inspected = inspected or [] geo_indices = [] inspected.append(cls) - EmbeddedDocumentField = _import_class("EmbeddedDocumentField") - GeoPointField = _import_class("GeoPointField") + geo_field_type_names = ["EmbeddedDocumentField", "GeoPointField", + "PointField", "LineStringField", "PolygonField"] + + geo_field_types = tuple([_import_class(field) for field in geo_field_type_names]) for field in cls._fields.values(): - if not isinstance(field, (EmbeddedDocumentField, GeoPointField)): + if not isinstance(field, geo_field_types): continue if hasattr(field, 'document_type'): field_cls = field.document_type if field_cls in inspected: continue if hasattr(field_cls, '_geo_indices'): - geo_indices += field_cls._geo_indices(inspected) + geo_indices += field_cls._geo_indices(inspected, parent_field=field.db_field) elif field._geo_index: + field_name = field.db_field + if parent_field: + field_name = "%s.%s" % (parent_field, field_name) geo_indices.append({'fields': - [(field.db_field, pymongo.GEO2D)]}) + [(field_name, field._geo_index)]}) return geo_indices @classmethod diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index d9ed2788..fa0b1348 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -2,7 +2,8 @@ import operator import warnings import weakref -from bson import DBRef, ObjectId +from bson import DBRef, ObjectId, SON +import pymongo from mongoengine.common import _import_class from mongoengine.errors import ValidationError @@ -10,7 +11,7 @@ from mongoengine.errors import ValidationError from mongoengine.base.common import ALLOW_INHERITANCE from mongoengine.base.datastructures import BaseDict, BaseList -__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField") +__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField") class BaseField(object): @@ -186,7 +187,7 @@ class ComplexBaseField(BaseField): # Convert lists / values so we can watch for any changes on them if (isinstance(value, (list, tuple)) and - not isinstance(value, BaseList)): + not isinstance(value, BaseList)): value = BaseList(value, instance, self.name) instance._data[self.name] = value elif isinstance(value, dict) and not isinstance(value, BaseDict): @@ -194,8 +195,8 @@ class ComplexBaseField(BaseField): instance._data[self.name] = value if (self._auto_dereference and instance._initialised and - isinstance(value, (BaseList, BaseDict)) - and not value._dereferenced): + isinstance(value, (BaseList, BaseDict)) + and not value._dereferenced): value = self._dereference( value, max_depth=1, instance=instance, name=self.name ) @@ -231,7 +232,7 @@ class ComplexBaseField(BaseField): if self.field: value_dict = dict([(key, self.field.to_python(item)) - for key, item in value.items()]) + for key, item in value.items()]) else: value_dict = {} for k, v in value.items(): @@ -282,7 +283,7 @@ class ComplexBaseField(BaseField): if self.field: value_dict = dict([(key, self.field.to_mongo(item)) - for key, item in value.iteritems()]) + for key, item in value.iteritems()]) else: value_dict = {} for k, v in value.iteritems(): @@ -396,3 +397,100 @@ class ObjectIdField(BaseField): ObjectId(unicode(value)) except: self.error('Invalid Object ID') + + +class GeoJsonBaseField(BaseField): + """A geo json field storing a geojson style object. + .. versionadded:: 0.8 + """ + + _geo_index = pymongo.GEOSPHERE + _type = "GeoBase" + + def __init__(self, auto_index=True, *args, **kwargs): + """ + :param auto_index: Automatically create a "2dsphere" index. Defaults + to `True`. + """ + self._name = "%sField" % self._type + if not auto_index: + self._geo_index = False + super(GeoJsonBaseField, self).__init__(*args, **kwargs) + + def validate(self, value): + """Validate the GeoJson object based on its type + """ + if isinstance(value, dict): + if set(value.keys()) == set(['type', 'coordinates']): + if value['type'] != self._type: + self.error('%s type must be "%s"' % (self._name, self._type)) + return self.validate(value['coordinates']) + else: + self.error('%s can only accept a valid GeoJson dictionary' + ' or lists of (x, y)' % self._name) + return + elif not isinstance(value, (list, tuple)): + self.error('%s can only accept lists of [x, y]' % self._name) + return + + validate = getattr(self, "_validate_%s" % self._type.lower()) + error = validate(value) + if error: + self.error(error) + + def _validate_polygon(self, value): + if not isinstance(value, (list, tuple)): + return 'Polygons must contain list of linestrings' + + # Quick and dirty validator + try: + value[0][0][0] + except: + return "Invalid Polygon must contain at least one valid linestring" + + errors = [] + for val in value: + error = self._validate_linestring(val, False) + if not error and val[0] != val[-1]: + error = 'LineStrings must start and end at the same point' + if error and error not in errors: + errors.append(error) + if errors: + return "Invalid Polygon:\n%s" % ", ".join(set(errors)) + + def _validate_linestring(self, value, top_level=True): + """Validates a linestring""" + if not isinstance(value, (list, tuple)): + return 'LineStrings must contain list of coordinate pairs' + + # Quick and dirty validator + try: + value[0][0] + except: + return "Invalid LineString must contain at least one valid point" + + errors = [] + for val in value: + error = self._validate_point(val) + if error and error not in errors: + errors.append(error) + if errors: + if top_level: + return "Invalid LineString:\n%s" % ", ".join(errors) + else: + return "%s" % ", ".join(set(errors)) + + def _validate_point(self, value): + """Validate each set of coords""" + if not isinstance(value, (list, tuple)): + return 'Points must be a list of coordinate pairs' + elif not len(value) == 2: + return "Value (%s) must be a two-dimensional point" % repr(value) + elif (not isinstance(value[0], (float, int)) or + not isinstance(value[1], (float, int))): + return "Both values (%s) in point must be float or int" % repr(value) + + def to_mongo(self, value): + if isinstance(value, dict): + return value + return SON([("type", self._type), ("coordinates", value)]) diff --git a/mongoengine/common.py b/mongoengine/common.py index 718ac0b2..bff55ac5 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -11,6 +11,7 @@ def _import_class(cls_name): field_classes = ('DictField', 'DynamicField', 'EmbeddedDocumentField', 'FileField', 'GenericReferenceField', 'GenericEmbeddedDocumentField', 'GeoPointField', + 'PointField', 'LineStringField', 'PolygonField', 'ReferenceField', 'StringField', 'ComplexBaseField') queryset_classes = ('OperationError',) deref_classes = ('DeReference',) @@ -33,4 +34,4 @@ def _import_class(cls_name): for cls in import_classes: _class_registry_cache[cls] = getattr(module, cls) - return _class_registry_cache.get(cls_name) \ No newline at end of file + return _class_registry_cache.get(cls_name) diff --git a/mongoengine/document.py b/mongoengine/document.py index bd6ce191..143802cc 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -523,7 +523,6 @@ class Document(BaseDocument): # an extra index on _cls, as mongodb will use the existing # index to service queries against _cls cls_indexed = False - def includes_cls(fields): first_field = None if len(fields): diff --git a/mongoengine/fields.py b/mongoengine/fields.py index bb2539cc..274ad3c7 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -15,7 +15,7 @@ from bson import Binary, DBRef, SON, ObjectId from mongoengine.errors import ValidationError from mongoengine.python_support import (PY3, bin_type, txt_type, str_types, StringIO) -from base import (BaseField, ComplexBaseField, ObjectIdField, +from base import (BaseField, ComplexBaseField, ObjectIdField, GeoJsonBaseField, get_document, BaseDocument) from queryset import DO_NOTHING, QuerySet from document import Document, EmbeddedDocument @@ -34,8 +34,8 @@ __all__ = ['StringField', 'URLField', 'EmailField', 'IntField', 'LongField', 'SortedListField', 'DictField', 'MapField', 'ReferenceField', 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy', - 'ImproperlyConfigured', 'ImageField', 'GeoPointField', - 'SequenceField', 'UUIDField'] + 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', + 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField'] RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -1386,28 +1386,6 @@ class ImageField(FileField): **kwargs) -class GeoPointField(BaseField): - """A list storing a latitude and longitude. - - .. versionadded:: 0.4 - """ - - _geo_index = pymongo.GEO2D - - def validate(self, value): - """Make sure that a geo-value is of type (x, y) - """ - if not isinstance(value, (list, tuple)): - self.error('GeoPointField can only accept tuples or lists ' - 'of (x, y)') - - if not len(value) == 2: - self.error('Value must be a two-dimensional point') - if (not isinstance(value[0], (float, int)) and - not isinstance(value[1], (float, int))): - self.error('Both values in point must be float or int') - - class SequenceField(BaseField): """Provides a sequental counter see: http://www.mongodb.org/display/DOCS/Object+IDs#ObjectIDs-SequenceNumbers @@ -1548,3 +1526,83 @@ class UUIDField(BaseField): value = uuid.UUID(value) except Exception, exc: self.error('Could not convert to UUID: %s' % exc) + + +class GeoPointField(BaseField): + """A list storing a latitude and longitude. + + .. versionadded:: 0.4 + """ + + _geo_index = pymongo.GEO2D + + def validate(self, value): + """Make sure that a geo-value is of type (x, y) + """ + if not isinstance(value, (list, tuple)): + self.error('GeoPointField can only accept tuples or lists ' + 'of (x, y)') + + if not len(value) == 2: + self.error("Value (%s) must be a two-dimensional point" % repr(value)) + elif (not isinstance(value[0], (float, int)) or + not isinstance(value[1], (float, int))): + self.error("Both values (%s) in point must be float or int" % repr(value)) + + +class PointField(GeoJsonBaseField): + """A geo json field storing a latitude and longitude. + + The data is represented as: + + .. code-block:: js + + { "type" : "Point" , + "coordinates" : [x, y]} + + You can either pass a dict with the full information or a list + to set the value. + + Requires mongodb >= 2.4 + .. versionadded:: 0.8 + """ + _type = "Point" + + +class LineStringField(GeoJsonBaseField): + """A geo json field storing a line of latitude and longitude coordinates. + + The data is represented as: + + .. code-block:: js + + { "type" : "LineString" , + "coordinates" : [[x1, y1], [x1, y1] ... [xn, yn]]} + + You can either pass a dict with the full information or a list of points. + + Requires mongodb >= 2.4 + .. versionadded:: 0.8 + """ + _type = "LineString" + + +class PolygonField(GeoJsonBaseField): + """A geo json field storing a polygon of latitude and longitude coordinates. + + The data is represented as: + + .. code-block:: js + + { "type" : "Polygon" , + "coordinates" : [[[x1, y1], [x1, y1] ... [xn, yn]], + [[x1, y1], [x1, y1] ... [xn, yn]]} + + You can either pass a dict with the full information or a list + of LineStrings. The first LineString being the outside and the rest being + holes. + + Requires mongodb >= 2.4 + .. versionadded:: 0.8 + """ + _type = "Polygon" diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 5ae889c5..bfb5a486 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1422,15 +1422,14 @@ class QuerySet(object): code = re.sub(u'\[\s*~([A-z_][A-z_0-9.]+?)\s*\]', field_sub, code) code = re.sub(u'\{\{\s*~([A-z_][A-z_0-9.]+?)\s*\}\}', field_path_sub, - code) + code) return code # Deprecated - def ensure_index(self, **kwargs): """Deprecated use :func:`~Document.ensure_index`""" msg = ("Doc.objects()._ensure_index() is deprecated. " - "Use Doc.ensure_index() instead.") + "Use Doc.ensure_index() instead.") warnings.warn(msg, DeprecationWarning) self._document.__class__.ensure_index(**kwargs) return self @@ -1438,6 +1437,6 @@ class QuerySet(object): def _ensure_indexes(self): """Deprecated use :func:`~Document.ensure_indexes`""" msg = ("Doc.objects()._ensure_indexes() is deprecated. " - "Use Doc.ensure_indexes() instead.") + "Use Doc.ensure_indexes() instead.") warnings.warn(msg, DeprecationWarning) self._document.__class__.ensure_indexes() diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 3da26935..96d99040 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -1,5 +1,6 @@ from collections import defaultdict +import pymongo from bson import SON from mongoengine.common import _import_class @@ -12,7 +13,9 @@ COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', 'all', 'size', 'exists', 'not') GEO_OPERATORS = ('within_distance', 'within_spherical_distance', 'within_box', 'within_polygon', 'near', 'near_sphere', - 'max_distance') + 'max_distance', 'geo_within', 'geo_within_box', + 'geo_within_polygon', 'geo_within_center', + 'geo_within_sphere', 'geo_intersects') STRING_OPERATORS = ('contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith', 'exact', 'iexact') @@ -81,30 +84,14 @@ def query(_doc_cls=None, _field_operation=False, **query): value = field else: value = field.prepare_query_value(op, value) - elif op in ('in', 'nin', 'all', 'near'): + elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict): # 'in', 'nin' and 'all' require a list of values value = [field.prepare_query_value(op, v) for v in value] # if op and op not in COMPARISON_OPERATORS: if op: if op in GEO_OPERATORS: - if op == "within_distance": - value = {'$within': {'$center': value}} - elif op == "within_spherical_distance": - value = {'$within': {'$centerSphere': value}} - elif op == "within_polygon": - value = {'$within': {'$polygon': value}} - elif op == "near": - value = {'$near': value} - elif op == "near_sphere": - value = {'$nearSphere': value} - elif op == 'within_box': - value = {'$within': {'$box': value}} - elif op == "max_distance": - value = {'$maxDistance': value} - else: - raise NotImplementedError("Geo method '%s' has not " - "been implemented" % op) + value = _geo_operator(field, op, value) elif op in CUSTOM_OPERATORS: if op == 'match': value = {"$elemMatch": value} @@ -250,3 +237,76 @@ def update(_doc_cls=None, **update): mongo_update[key].update(value) return mongo_update + + +def _geo_operator(field, op, value): + """Helper to return the query for a given geo query""" + if field._geo_index == pymongo.GEO2D: + if op == "within_distance": + value = {'$within': {'$center': value}} + elif op == "within_spherical_distance": + value = {'$within': {'$centerSphere': value}} + elif op == "within_polygon": + value = {'$within': {'$polygon': value}} + elif op == "near": + value = {'$near': value} + elif op == "near_sphere": + value = {'$nearSphere': value} + elif op == 'within_box': + value = {'$within': {'$box': value}} + elif op == "max_distance": + value = {'$maxDistance': value} + else: + raise NotImplementedError("Geo method '%s' has not " + "been implemented for a GeoPointField" % op) + else: + if op == "geo_within": + value = {"$geoWithin": _infer_geometry(value)} + elif op == "geo_within_box": + value = {"$geoWithin": {"$box": value}} + elif op == "geo_within_polygon": + value = {"$geoWithin": {"$polygon": value}} + elif op == "geo_within_center": + value = {"$geoWithin": {"$center": value}} + elif op == "geo_within_sphere": + value = {"$geoWithin": {"$centerSphere": value}} + elif op == "geo_intersects": + value = {"$geoIntersects": _infer_geometry(value)} + elif op == "near": + value = {'$near': _infer_geometry(value)} + elif op == "max_distance": + value = {'$maxDistance': value} + else: + raise NotImplementedError("Geo method '%s' has not " + "been implemented for a %s " % (op, field._name)) + return value + + +def _infer_geometry(value): + """Helper method that tries to infer the $geometry shape for a given value""" + if isinstance(value, dict): + if "$geometry" in value: + return value + elif 'coordinates' in value and 'type' in value: + return {"$geometry": value} + raise InvalidQueryError("Invalid $geometry dictionary should have " + "type and coordinates keys") + elif isinstance(value, (list, set)): + try: + value[0][0][0] + return {"$geometry": {"type": "Polygon", "coordinates": value}} + except: + pass + try: + value[0][0] + return {"$geometry": {"type": "LineString", "coordinates": value}} + except: + pass + try: + value[0] + return {"$geometry": {"type": "Point", "coordinates": value}} + except: + pass + + raise InvalidQueryError("Invalid $geometry data. Can be either a dictionary " + "or (nested) lists of coordinate(s)") diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 99aeca6d..ddc147b3 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -381,8 +381,7 @@ class IndexesTest(unittest.TestCase): self.assertEqual(sorted(info.keys()), ['_id_', 'tags.tag_1']) post1 = BlogPost(title="Embedded Indexes tests in place", - tags=[Tag(name="about"), Tag(name="time")] - ) + tags=[Tag(name="about"), Tag(name="time")]) post1.save() BlogPost.drop_collection() @@ -399,29 +398,6 @@ class IndexesTest(unittest.TestCase): info = RecursiveDocument._get_collection().index_information() self.assertEqual(sorted(info.keys()), ['_cls_1', '_id_']) - def test_geo_indexes_recursion(self): - - class Location(Document): - name = StringField() - location = GeoPointField() - - class Parent(Document): - name = StringField() - location = ReferenceField(Location, dbref=False) - - Location.drop_collection() - Parent.drop_collection() - - list(Parent.objects) - - collection = Parent._get_collection() - info = collection.index_information() - - self.assertFalse('location_2d' in info) - - self.assertEqual(len(Parent._geo_indices()), 0) - self.assertEqual(len(Location._geo_indices()), 1) - def test_covered_index(self): """Ensure that covered indexes can be used """ @@ -432,7 +408,7 @@ class IndexesTest(unittest.TestCase): meta = { 'indexes': ['a'], 'allow_inheritance': False - } + } Test.drop_collection() diff --git a/tests/fields/__init__.py b/tests/fields/__init__.py index 0731838b..be70aaaa 100644 --- a/tests/fields/__init__.py +++ b/tests/fields/__init__.py @@ -1,2 +1,3 @@ from fields import * -from file_tests import * \ No newline at end of file +from file_tests import * +from geo import * \ No newline at end of file diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 5474aa6f..f7ab63ea 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1862,45 +1862,6 @@ class FieldTest(unittest.TestCase): Shirt.drop_collection() - def test_geo_indexes(self): - """Ensure that indexes are created automatically for GeoPointFields. - """ - class Event(Document): - title = StringField() - location = GeoPointField() - - Event.drop_collection() - event = Event(title="Coltrane Motion @ Double Door", - location=[41.909889, -87.677137]) - event.save() - - info = Event.objects._collection.index_information() - self.assertTrue(u'location_2d' in info) - self.assertTrue(info[u'location_2d']['key'] == [(u'location', u'2d')]) - - Event.drop_collection() - - def test_geo_embedded_indexes(self): - """Ensure that indexes are created automatically for GeoPointFields on - embedded documents. - """ - class Venue(EmbeddedDocument): - location = GeoPointField() - name = StringField() - - class Event(Document): - title = StringField() - venue = EmbeddedDocumentField(Venue) - - Event.drop_collection() - venue = Venue(name="Double Door", location=[41.909889, -87.677137]) - event = Event(title="Coltrane Motion", venue=venue) - event.save() - - info = Event.objects._collection.index_information() - self.assertTrue(u'location_2d' in info) - self.assertTrue(info[u'location_2d']['key'] == [(u'location', u'2d')]) - def test_ensure_unique_default_instances(self): """Ensure that every field has it's own unique default instance.""" class D(Document): diff --git a/tests/fields/geo.py b/tests/fields/geo.py new file mode 100644 index 00000000..2936f72a --- /dev/null +++ b/tests/fields/geo.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +import sys +sys.path[0:0] = [""] + +import unittest + +from mongoengine import * +from mongoengine.connection import get_db + +__all__ = ("GeoFieldTest", ) + + +class GeoFieldTest(unittest.TestCase): + + def setUp(self): + connect(db='mongoenginetest') + self.db = get_db() + + def _test_for_expected_error(self, Cls, loc, expected): + try: + Cls(loc=loc).validate() + self.fail() + except ValidationError, e: + self.assertEqual(expected, e.to_dict()['loc']) + + def test_geopoint_validation(self): + class Location(Document): + loc = GeoPointField() + + invalid_coords = [{"x": 1, "y": 2}, 5, "a"] + expected = 'GeoPointField can only accept tuples or lists of (x, y)' + + for coord in invalid_coords: + self._test_for_expected_error(Location, coord, expected) + + invalid_coords = [[], [1], [1, 2, 3]] + for coord in invalid_coords: + expected = "Value (%s) must be a two-dimensional point" % repr(coord) + self._test_for_expected_error(Location, coord, expected) + + invalid_coords = [[{}, {}], ("a", "b")] + for coord in invalid_coords: + expected = "Both values (%s) in point must be float or int" % repr(coord) + self._test_for_expected_error(Location, coord, expected) + + def test_point_validation(self): + class Location(Document): + loc = PointField() + + invalid_coords = {"x": 1, "y": 2} + expected = 'PointField can only accept a valid GeoJson dictionary or lists of (x, y)' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MadeUp", "coordinates": []} + expected = 'PointField type must be "Point"' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "Point", "coordinates": [1, 2, 3]} + expected = "Value ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [5, "a"] + expected = "PointField can only accept lists of [x, y]" + for coord in invalid_coords: + self._test_for_expected_error(Location, coord, expected) + + invalid_coords = [[], [1], [1, 2, 3]] + for coord in invalid_coords: + expected = "Value (%s) must be a two-dimensional point" % repr(coord) + self._test_for_expected_error(Location, coord, expected) + + invalid_coords = [[{}, {}], ("a", "b")] + for coord in invalid_coords: + expected = "Both values (%s) in point must be float or int" % repr(coord) + self._test_for_expected_error(Location, coord, expected) + + Location(loc=[1, 2]).validate() + + def test_linestring_validation(self): + class Location(Document): + loc = LineStringField() + + invalid_coords = {"x": 1, "y": 2} + expected = 'LineStringField can only accept a valid GeoJson dictionary or lists of (x, y)' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MadeUp", "coordinates": [[]]} + expected = 'LineStringField type must be "LineString"' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "LineString", "coordinates": [[1, 2, 3]]} + expected = "Invalid LineString:\nValue ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [5, "a"] + expected = "Invalid LineString must contain at least one valid point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[1]] + expected = "Invalid LineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0]) + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[1, 2, 3]] + expected = "Invalid LineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0]) + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[{}, {}]], [("a", "b")]] + for coord in invalid_coords: + expected = "Invalid LineString:\nBoth values (%s) in point must be float or int" % repr(coord[0]) + self._test_for_expected_error(Location, coord, expected) + + Location(loc=[[1, 2], [3, 4], [5, 6], [1,2]]).validate() + + def test_polygon_validation(self): + class Location(Document): + loc = PolygonField() + + invalid_coords = {"x": 1, "y": 2} + expected = 'PolygonField can only accept a valid GeoJson dictionary or lists of (x, y)' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MadeUp", "coordinates": [[]]} + expected = 'PolygonField type must be "Polygon"' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "Polygon", "coordinates": [[[1, 2, 3]]]} + expected = "Invalid Polygon:\nValue ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[5, "a"]]] + expected = "Invalid Polygon:\nBoth values ([5, 'a']) in point must be float or int" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[]]] + expected = "Invalid Polygon must contain at least one valid linestring" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[1, 2, 3]]] + expected = "Invalid Polygon:\nValue ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[{}, {}]], [("a", "b")]] + expected = "Invalid Polygon:\nBoth values ([{}, {}]) in point must be float or int, Both values (('a', 'b')) in point must be float or int" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[1, 2], [3, 4]]] + expected = "Invalid Polygon:\nLineStrings must start and end at the same point" + self._test_for_expected_error(Location, invalid_coords, expected) + + Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate() + + def test_indexes_geopoint(self): + """Ensure that indexes are created automatically for GeoPointFields. + """ + class Event(Document): + title = StringField() + location = GeoPointField() + + geo_indicies = Event._geo_indices() + self.assertEqual(geo_indicies, [{'fields': [('location', '2d')]}]) + + def test_geopoint_embedded_indexes(self): + """Ensure that indexes are created automatically for GeoPointFields on + embedded documents. + """ + class Venue(EmbeddedDocument): + location = GeoPointField() + name = StringField() + + class Event(Document): + title = StringField() + venue = EmbeddedDocumentField(Venue) + + geo_indicies = Event._geo_indices() + self.assertEqual(geo_indicies, [{'fields': [('venue.location', '2d')]}]) + + def test_indexes_2dsphere(self): + """Ensure that indexes are created automatically for GeoPointFields. + """ + class Event(Document): + title = StringField() + point = PointField() + line = LineStringField() + polygon = PolygonField() + + geo_indicies = Event._geo_indices() + self.assertEqual(geo_indicies, [{'fields': [('line', '2dsphere')]}, + {'fields': [('polygon', '2dsphere')]}, + {'fields': [('point', '2dsphere')]}]) + + def test_indexes_2dsphere_embedded(self): + """Ensure that indexes are created automatically for GeoPointFields. + """ + class Venue(EmbeddedDocument): + name = StringField() + point = PointField() + line = LineStringField() + polygon = PolygonField() + + class Event(Document): + title = StringField() + venue = EmbeddedDocumentField(Venue) + + geo_indicies = Event._geo_indices() + self.assertTrue({'fields': [('venue.line', '2dsphere')]} in geo_indicies) + self.assertTrue({'fields': [('venue.polygon', '2dsphere')]} in geo_indicies) + self.assertTrue({'fields': [('venue.point', '2dsphere')]} in geo_indicies) + + def test_geo_indexes_recursion(self): + + class Location(Document): + name = StringField() + location = GeoPointField() + + class Parent(Document): + name = StringField() + location = ReferenceField(Location) + + Location.drop_collection() + Parent.drop_collection() + + list(Parent.objects) + + collection = Parent._get_collection() + info = collection.index_information() + + self.assertFalse('location_2d' in info) + + self.assertEqual(len(Parent._geo_indices()), 0) + self.assertEqual(len(Location._geo_indices()), 1) + + def test_geo_indexes_auto_index(self): + + # Test just listing the fields + class Log(Document): + location = PointField(auto_index=False) + datetime = DateTimeField() + + meta = { + 'indexes': [[("location", "2dsphere"), ("datetime", 1)]] + } + + self.assertEqual([], Log._geo_indices()) + + Log.drop_collection() + Log.ensure_indexes() + + info = Log._get_collection().index_information() + self.assertEqual(info["location_2dsphere_datetime_1"]["key"], + [('location', '2dsphere'), ('datetime', 1)]) + + # Test listing explicitly + class Log(Document): + location = PointField(auto_index=False) + datetime = DateTimeField() + + meta = { + 'indexes': [ + {'fields': [("location", "2dsphere"), ("datetime", 1)]} + ] + } + + self.assertEqual([], Log._geo_indices()) + + Log.drop_collection() + Log.ensure_indexes() + + info = Log._get_collection().index_information() + self.assertEqual(info["location_2dsphere_datetime_1"]["key"], + [('location', '2dsphere'), ('datetime', 1)]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/queryset/__init__.py b/tests/queryset/__init__.py index 93cb8c23..8a93c19f 100644 --- a/tests/queryset/__init__.py +++ b/tests/queryset/__init__.py @@ -1,5 +1,5 @@ - from transform import * from field_list import * from queryset import * -from visitor import * \ No newline at end of file +from visitor import * +from geo import * diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py new file mode 100644 index 00000000..f5648961 --- /dev/null +++ b/tests/queryset/geo.py @@ -0,0 +1,418 @@ +import sys +sys.path[0:0] = [""] + +import unittest +from datetime import datetime, timedelta +from mongoengine import * + +__all__ = ("GeoQueriesTest",) + + +class GeoQueriesTest(unittest.TestCase): + + def setUp(self): + connect(db='mongoenginetest') + + def test_geospatial_operators(self): + """Ensure that geospatial queries are working. + """ + class Event(Document): + title = StringField() + date = DateTimeField() + location = GeoPointField() + + def __unicode__(self): + return self.title + + Event.drop_collection() + + event1 = Event(title="Coltrane Motion @ Double Door", + date=datetime.now() - timedelta(days=1), + location=[-87.677137, 41.909889]).save() + event2 = Event(title="Coltrane Motion @ Bottom of the Hill", + date=datetime.now() - timedelta(days=10), + location=[-122.4194155, 37.7749295]).save() + event3 = Event(title="Coltrane Motion @ Empty Bottle", + date=datetime.now(), + location=[-87.686638, 41.900474]).save() + + # find all events "near" pitchfork office, chicago. + # note that "near" will show the san francisco event, too, + # although it sorts to last. + events = Event.objects(location__near=[-87.67892, 41.9120459]) + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event1, event3, event2]) + + # find events within 5 degrees of pitchfork office, chicago + point_and_distance = [[-87.67892, 41.9120459], 5] + events = Event.objects(location__within_distance=point_and_distance) + self.assertEqual(events.count(), 2) + events = list(events) + self.assertTrue(event2 not in events) + self.assertTrue(event1 in events) + self.assertTrue(event3 in events) + + # ensure ordering is respected by "near" + events = Event.objects(location__near=[-87.67892, 41.9120459]) + events = events.order_by("-date") + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event3, event1, event2]) + + # find events within 10 degrees of san francisco + point = [-122.415579, 37.7566023] + events = Event.objects(location__near=point, location__max_distance=10) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event2) + + # find events within 10 degrees of san francisco + point_and_distance = [[-122.415579, 37.7566023], 10] + events = Event.objects(location__within_distance=point_and_distance) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event2) + + # find events within 1 degree of greenpoint, broolyn, nyc, ny + point_and_distance = [[-73.9509714, 40.7237134], 1] + events = Event.objects(location__within_distance=point_and_distance) + self.assertEqual(events.count(), 0) + + # ensure ordering is respected by "within_distance" + point_and_distance = [[-87.67892, 41.9120459], 10] + events = Event.objects(location__within_distance=point_and_distance) + events = events.order_by("-date") + self.assertEqual(events.count(), 2) + self.assertEqual(events[0], event3) + + # check that within_box works + box = [(-125.0, 35.0), (-100.0, 40.0)] + events = Event.objects(location__within_box=box) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0].id, event2.id) + + polygon = [ + (-87.694445, 41.912114), + (-87.69084, 41.919395), + (-87.681742, 41.927186), + (-87.654276, 41.911731), + (-87.656164, 41.898061), + ] + events = Event.objects(location__within_polygon=polygon) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0].id, event1.id) + + polygon2 = [ + (-1.742249, 54.033586), + (-1.225891, 52.792797), + (-4.40094, 53.389881) + ] + events = Event.objects(location__within_polygon=polygon2) + self.assertEqual(events.count(), 0) + + def test_geo_spatial_embedded(self): + + class Venue(EmbeddedDocument): + location = GeoPointField() + name = StringField() + + class Event(Document): + title = StringField() + venue = EmbeddedDocumentField(Venue) + + Event.drop_collection() + + venue1 = Venue(name="The Rock", location=[-87.677137, 41.909889]) + venue2 = Venue(name="The Bridge", location=[-122.4194155, 37.7749295]) + + event1 = Event(title="Coltrane Motion @ Double Door", + venue=venue1).save() + event2 = Event(title="Coltrane Motion @ Bottom of the Hill", + venue=venue2).save() + event3 = Event(title="Coltrane Motion @ Empty Bottle", + venue=venue1).save() + + # find all events "near" pitchfork office, chicago. + # note that "near" will show the san francisco event, too, + # although it sorts to last. + events = Event.objects(venue__location__near=[-87.67892, 41.9120459]) + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event1, event3, event2]) + + def test_spherical_geospatial_operators(self): + """Ensure that spherical geospatial queries are working + """ + class Point(Document): + location = GeoPointField() + + Point.drop_collection() + + # These points are one degree apart, which (according to Google Maps) + # is about 110 km apart at this place on the Earth. + north_point = Point(location=[-122, 38]).save() # Near Concord, CA + south_point = Point(location=[-122, 37]).save() # Near Santa Cruz, CA + + earth_radius = 6378.009 # in km (needs to be a float for dividing by) + + # Finds both points because they are within 60 km of the reference + # point equidistant between them. + points = Point.objects(location__near_sphere=[-122, 37.5]) + self.assertEqual(points.count(), 2) + + # Same behavior for _within_spherical_distance + points = Point.objects( + location__within_spherical_distance=[[-122, 37.5], 60/earth_radius] + ) + self.assertEqual(points.count(), 2) + + points = Point.objects(location__near_sphere=[-122, 37.5], + location__max_distance=60 / earth_radius) + self.assertEqual(points.count(), 2) + + # Finds both points, but orders the north point first because it's + # closer to the reference point to the north. + points = Point.objects(location__near_sphere=[-122, 38.5]) + self.assertEqual(points.count(), 2) + self.assertEqual(points[0].id, north_point.id) + self.assertEqual(points[1].id, south_point.id) + + # Finds both points, but orders the south point first because it's + # closer to the reference point to the south. + points = Point.objects(location__near_sphere=[-122, 36.5]) + self.assertEqual(points.count(), 2) + self.assertEqual(points[0].id, south_point.id) + self.assertEqual(points[1].id, north_point.id) + + # Finds only one point because only the first point is within 60km of + # the reference point to the south. + points = Point.objects( + location__within_spherical_distance=[[-122, 36.5], 60/earth_radius]) + self.assertEqual(points.count(), 1) + self.assertEqual(points[0].id, south_point.id) + + def test_2dsphere_point(self): + + class Event(Document): + title = StringField() + date = DateTimeField() + location = PointField() + + def __unicode__(self): + return self.title + + Event.drop_collection() + + event1 = Event(title="Coltrane Motion @ Double Door", + date=datetime.now() - timedelta(days=1), + location=[-87.677137, 41.909889]) + event1.save() + event2 = Event(title="Coltrane Motion @ Bottom of the Hill", + date=datetime.now() - timedelta(days=10), + location=[-122.4194155, 37.7749295]).save() + event3 = Event(title="Coltrane Motion @ Empty Bottle", + date=datetime.now(), + location=[-87.686638, 41.900474]).save() + + # find all events "near" pitchfork office, chicago. + # note that "near" will show the san francisco event, too, + # although it sorts to last. + events = Event.objects(location__near=[-87.67892, 41.9120459]) + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event1, event3, event2]) + + # find events within 5 degrees of pitchfork office, chicago + point_and_distance = [[-87.67892, 41.9120459], 2] + events = Event.objects(location__geo_within_center=point_and_distance) + self.assertEqual(events.count(), 2) + events = list(events) + self.assertTrue(event2 not in events) + self.assertTrue(event1 in events) + self.assertTrue(event3 in events) + + # ensure ordering is respected by "near" + events = Event.objects(location__near=[-87.67892, 41.9120459]) + events = events.order_by("-date") + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event3, event1, event2]) + + # find events within 10km of san francisco + point = [-122.415579, 37.7566023] + events = Event.objects(location__near=point, location__max_distance=10000) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event2) + + # find events within 1km of greenpoint, broolyn, nyc, ny + events = Event.objects(location__near=[-73.9509714, 40.7237134], location__max_distance=1000) + self.assertEqual(events.count(), 0) + + # ensure ordering is respected by "near" + events = Event.objects(location__near=[-87.67892, 41.9120459], + location__max_distance=10000).order_by("-date") + self.assertEqual(events.count(), 2) + self.assertEqual(events[0], event3) + + # check that within_box works + box = [(-125.0, 35.0), (-100.0, 40.0)] + events = Event.objects(location__geo_within_box=box) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0].id, event2.id) + + polygon = [ + (-87.694445, 41.912114), + (-87.69084, 41.919395), + (-87.681742, 41.927186), + (-87.654276, 41.911731), + (-87.656164, 41.898061), + ] + events = Event.objects(location__geo_within_polygon=polygon) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0].id, event1.id) + + polygon2 = [ + (-1.742249, 54.033586), + (-1.225891, 52.792797), + (-4.40094, 53.389881) + ] + events = Event.objects(location__geo_within_polygon=polygon2) + self.assertEqual(events.count(), 0) + + def test_2dsphere_point_embedded(self): + + class Venue(EmbeddedDocument): + location = GeoPointField() + name = StringField() + + class Event(Document): + title = StringField() + venue = EmbeddedDocumentField(Venue) + + Event.drop_collection() + + venue1 = Venue(name="The Rock", location=[-87.677137, 41.909889]) + venue2 = Venue(name="The Bridge", location=[-122.4194155, 37.7749295]) + + event1 = Event(title="Coltrane Motion @ Double Door", + venue=venue1).save() + event2 = Event(title="Coltrane Motion @ Bottom of the Hill", + venue=venue2).save() + event3 = Event(title="Coltrane Motion @ Empty Bottle", + venue=venue1).save() + + # find all events "near" pitchfork office, chicago. + # note that "near" will show the san francisco event, too, + # although it sorts to last. + events = Event.objects(venue__location__near=[-87.67892, 41.9120459]) + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event1, event3, event2]) + + def test_linestring(self): + + class Road(Document): + name = StringField() + line = LineStringField() + + Road.drop_collection() + + Road(name="66", line=[[40, 5], [41, 6]]).save() + + # near + point = {"type": "Point", "coordinates": [40, 5]} + roads = Road.objects.filter(line__near=point["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__near=point).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__near={"$geometry": point}).count() + self.assertEqual(1, roads) + + # Within + polygon = {"type": "Polygon", + "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]} + roads = Road.objects.filter(line__geo_within=polygon["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__geo_within=polygon).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__geo_within={"$geometry": polygon}).count() + self.assertEqual(1, roads) + + # Intersects + line = {"type": "LineString", + "coordinates": [[40, 5], [40, 6]]} + roads = Road.objects.filter(line__geo_intersects=line["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__geo_intersects=line).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__geo_intersects={"$geometry": line}).count() + self.assertEqual(1, roads) + + polygon = {"type": "Polygon", + "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]} + roads = Road.objects.filter(line__geo_intersects=polygon["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__geo_intersects=polygon).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(line__geo_intersects={"$geometry": polygon}).count() + self.assertEqual(1, roads) + + def test_polygon(self): + + class Road(Document): + name = StringField() + poly = PolygonField() + + Road.drop_collection() + + Road(name="66", poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]).save() + + # near + point = {"type": "Point", "coordinates": [40, 5]} + roads = Road.objects.filter(poly__near=point["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__near=point).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__near={"$geometry": point}).count() + self.assertEqual(1, roads) + + # Within + polygon = {"type": "Polygon", + "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]} + roads = Road.objects.filter(poly__geo_within=polygon["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__geo_within=polygon).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__geo_within={"$geometry": polygon}).count() + self.assertEqual(1, roads) + + # Intersects + line = {"type": "LineString", + "coordinates": [[40, 5], [41, 6]]} + roads = Road.objects.filter(poly__geo_intersects=line["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__geo_intersects=line).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__geo_intersects={"$geometry": line}).count() + self.assertEqual(1, roads) + + polygon = {"type": "Polygon", + "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]} + roads = Road.objects.filter(poly__geo_intersects=polygon["coordinates"]).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__geo_intersects=polygon).count() + self.assertEqual(1, roads) + + roads = Road.objects.filter(poly__geo_intersects={"$geometry": polygon}).count() + self.assertEqual(1, roads) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 5bf81835..40aef7ec 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2380,167 +2380,6 @@ class QuerySetTest(unittest.TestCase): def tearDown(self): self.Person.drop_collection() - def test_geospatial_operators(self): - """Ensure that geospatial queries are working. - """ - class Event(Document): - title = StringField() - date = DateTimeField() - location = GeoPointField() - - def __unicode__(self): - return self.title - - Event.drop_collection() - - event1 = Event(title="Coltrane Motion @ Double Door", - date=datetime.now() - timedelta(days=1), - location=[41.909889, -87.677137]) - event2 = Event(title="Coltrane Motion @ Bottom of the Hill", - date=datetime.now() - timedelta(days=10), - location=[37.7749295, -122.4194155]) - event3 = Event(title="Coltrane Motion @ Empty Bottle", - date=datetime.now(), - location=[41.900474, -87.686638]) - - event1.save() - event2.save() - event3.save() - - # find all events "near" pitchfork office, chicago. - # note that "near" will show the san francisco event, too, - # although it sorts to last. - events = Event.objects(location__near=[41.9120459, -87.67892]) - self.assertEqual(events.count(), 3) - self.assertEqual(list(events), [event1, event3, event2]) - - # find events within 5 degrees of pitchfork office, chicago - point_and_distance = [[41.9120459, -87.67892], 5] - events = Event.objects(location__within_distance=point_and_distance) - self.assertEqual(events.count(), 2) - events = list(events) - self.assertTrue(event2 not in events) - self.assertTrue(event1 in events) - self.assertTrue(event3 in events) - - # ensure ordering is respected by "near" - events = Event.objects(location__near=[41.9120459, -87.67892]) - events = events.order_by("-date") - self.assertEqual(events.count(), 3) - self.assertEqual(list(events), [event3, event1, event2]) - - # find events within 10 degrees of san francisco - point = [37.7566023, -122.415579] - events = Event.objects(location__near=point, location__max_distance=10) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0], event2) - - # find events within 10 degrees of san francisco - point_and_distance = [[37.7566023, -122.415579], 10] - events = Event.objects(location__within_distance=point_and_distance) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0], event2) - - # find events within 1 degree of greenpoint, broolyn, nyc, ny - point_and_distance = [[40.7237134, -73.9509714], 1] - events = Event.objects(location__within_distance=point_and_distance) - self.assertEqual(events.count(), 0) - - # ensure ordering is respected by "within_distance" - point_and_distance = [[41.9120459, -87.67892], 10] - events = Event.objects(location__within_distance=point_and_distance) - events = events.order_by("-date") - self.assertEqual(events.count(), 2) - self.assertEqual(events[0], event3) - - # check that within_box works - box = [(35.0, -125.0), (40.0, -100.0)] - events = Event.objects(location__within_box=box) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0].id, event2.id) - - # check that polygon works for users who have a server >= 1.9 - server_version = tuple( - get_connection().server_info()['version'].split('.') - ) - required_version = tuple("1.9.0".split(".")) - if server_version >= required_version: - polygon = [ - (41.912114,-87.694445), - (41.919395,-87.69084), - (41.927186,-87.681742), - (41.911731,-87.654276), - (41.898061,-87.656164), - ] - events = Event.objects(location__within_polygon=polygon) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0].id, event1.id) - - polygon2 = [ - (54.033586,-1.742249), - (52.792797,-1.225891), - (53.389881,-4.40094) - ] - events = Event.objects(location__within_polygon=polygon2) - self.assertEqual(events.count(), 0) - - Event.drop_collection() - - def test_spherical_geospatial_operators(self): - """Ensure that spherical geospatial queries are working - """ - class Point(Document): - location = GeoPointField() - - Point.drop_collection() - - # These points are one degree apart, which (according to Google Maps) - # is about 110 km apart at this place on the Earth. - north_point = Point(location=[-122, 38]) # Near Concord, CA - south_point = Point(location=[-122, 37]) # Near Santa Cruz, CA - north_point.save() - south_point.save() - - earth_radius = 6378.009; # in km (needs to be a float for dividing by) - - # Finds both points because they are within 60 km of the reference - # point equidistant between them. - points = Point.objects(location__near_sphere=[-122, 37.5]) - self.assertEqual(points.count(), 2) - - # Same behavior for _within_spherical_distance - points = Point.objects( - location__within_spherical_distance=[[-122, 37.5], 60/earth_radius] - ); - self.assertEqual(points.count(), 2) - - points = Point.objects(location__near_sphere=[-122, 37.5], - location__max_distance=60 / earth_radius); - self.assertEqual(points.count(), 2) - - # Finds both points, but orders the north point first because it's - # closer to the reference point to the north. - points = Point.objects(location__near_sphere=[-122, 38.5]) - self.assertEqual(points.count(), 2) - self.assertEqual(points[0].id, north_point.id) - self.assertEqual(points[1].id, south_point.id) - - # Finds both points, but orders the south point first because it's - # closer to the reference point to the south. - points = Point.objects(location__near_sphere=[-122, 36.5]) - self.assertEqual(points.count(), 2) - self.assertEqual(points[0].id, south_point.id) - self.assertEqual(points[1].id, north_point.id) - - # Finds only one point because only the first point is within 60km of - # the reference point to the south. - points = Point.objects( - location__within_spherical_distance=[[-122, 36.5], 60/earth_radius]) - self.assertEqual(points.count(), 1) - self.assertEqual(points[0].id, south_point.id) - - Point.drop_collection() - def test_custom_querysets(self): """Ensure that custom QuerySet classes may be used. """ From 68f760b56375255173bb31b04af485f5987c96da Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Apr 2013 15:05:41 +0000 Subject: [PATCH 11/40] get_db() only assigns the db after authentication (#257) --- mongoengine/connection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 3c53ea3c..abab269f 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -137,11 +137,12 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False): if alias not in _dbs: conn = get_connection(alias) conn_settings = _connection_settings[alias] - _dbs[alias] = conn[conn_settings['name']] + db = conn[conn_settings['name']] # Authenticate if necessary if conn_settings['username'] and conn_settings['password']: - _dbs[alias].authenticate(conn_settings['username'], - conn_settings['password']) + db.authenticate(conn_settings['username'], + conn_settings['password']) + _dbs[alias] = db return _dbs[alias] From 473d5ead7bf2b61e3b7dbab2845e496994ef5aed Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Apr 2013 16:42:38 +0000 Subject: [PATCH 12/40] Geo errors fix and test update --- mongoengine/base/fields.py | 4 ++-- tests/fields/geo.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index fa0b1348..72a9e8eb 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -456,7 +456,7 @@ class GeoJsonBaseField(BaseField): if error and error not in errors: errors.append(error) if errors: - return "Invalid Polygon:\n%s" % ", ".join(set(errors)) + return "Invalid Polygon:\n%s" % ", ".join(errors) def _validate_linestring(self, value, top_level=True): """Validates a linestring""" @@ -478,7 +478,7 @@ class GeoJsonBaseField(BaseField): if top_level: return "Invalid LineString:\n%s" % ", ".join(errors) else: - return "%s" % ", ".join(set(errors)) + return "%s" % ", ".join(errors) def _validate_point(self, value): """Validate each set of coords""" diff --git a/tests/fields/geo.py b/tests/fields/geo.py index 2936f72a..31ded26a 100644 --- a/tests/fields/geo.py +++ b/tests/fields/geo.py @@ -184,9 +184,9 @@ class GeoFieldTest(unittest.TestCase): polygon = PolygonField() geo_indicies = Event._geo_indices() - self.assertEqual(geo_indicies, [{'fields': [('line', '2dsphere')]}, - {'fields': [('polygon', '2dsphere')]}, - {'fields': [('point', '2dsphere')]}]) + self.assertTrue({'fields': [('line', '2dsphere')]} in geo_indicies) + self.assertTrue({'fields': [('polygon', '2dsphere')]} in geo_indicies) + self.assertTrue({'fields': [('point', '2dsphere')]} in geo_indicies) def test_indexes_2dsphere_embedded(self): """Ensure that indexes are created automatically for GeoPointFields. From 7aa1f473785ed17cff280835285a69e401fd9b86 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Apr 2013 16:46:08 +0000 Subject: [PATCH 13/40] Updated minimum requirements --- .travis.yml | 1 - docs/changelog.rst | 1 + docs/upgrade.rst | 6 +++--- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e78bda5a..b7c56a02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ env: - PYMONGO=dev DJANGO=1.4.2 - PYMONGO=2.5 DJANGO=1.5.1 - PYMONGO=2.5 DJANGO=1.4.2 - - PYMONGO=2.4.2 DJANGO=1.4.2 install: - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi diff --git a/docs/changelog.rst b/docs/changelog.rst index 207f0dd6..6aa62148 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.X ================ +- Updated minimum requirement for pymongo to 2.5 - Add support for new geojson fields, indexes and queries (#299) - If values cant be compared mark as changed (#287) - Ensure as_pymongo() and to_json honour only() and exclude() (#293) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index bb5705ca..c633c28d 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -15,10 +15,10 @@ possible for the whole of the release. live. There maybe multiple manual steps in migrating and these are best honed on a staging / test system. -Python -======= +Python and PyMongo +================== -Support for python 2.5 has been dropped. +MongoEngine requires python 2.6 (or above) and pymongo 2.5 (or above) Data Model ========== diff --git a/setup.py b/setup.py index bdd01825..10a6dbcb 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup(name='mongoengine', long_description=LONG_DESCRIPTION, platforms=['any'], classifiers=CLASSIFIERS, - install_requires=['pymongo'], + install_requires=['pymongo>=2.5'], test_suite='nose.collector', **extra_opts ) From 1c345edc49b9b5e382fdb8b64ab6bf5058d48288 Mon Sep 17 00:00:00 2001 From: Alex Kelly Date: Tue, 30 Apr 2013 21:36:43 +0100 Subject: [PATCH 14/40] Updated tests for passing write_concern to update and update_one to check return. --- tests/queryset/queryset.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 40aef7ec..7ca0596a 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -287,15 +287,19 @@ class QuerySetTest(unittest.TestCase): name='Test User', write_concern=write_concern) author.save(write_concern=write_concern) - self.Person.objects.update(set__name='Ross', - write_concern=write_concern) + result = self.Person.objects.update( + set__name='Ross',write_concern={"w": 1}) + self.assertEqual(result, 1) + result = self.Person.objects.update( + set__name='Ross',write_concern={"w": 0}) + self.assertEqual(result, None) - author = self.Person.objects.first() - self.assertEqual(author.name, 'Ross') - - self.Person.objects.update_one(set__name='Test User', write_concern=write_concern) - author = self.Person.objects.first() - self.assertEqual(author.name, 'Test User') + result = self.Person.objects.update_one( + set__name='Test User', write_concern={"w": 1}) + self.assertEqual(result, 1) + result = self.Person.objects.update_one( + set__name='Test User', write_concern={"w": 0}) + self.assertEqual(result, None) def test_update_update_has_a_value(self): """Test to ensure that update is passed a value to update to""" From 00a57f6cea8ba679281deba4fd4362ca23fb06c8 Mon Sep 17 00:00:00 2001 From: Alex Kelly Date: Tue, 30 Apr 2013 21:13:49 +0100 Subject: [PATCH 15/40] Pass write_concern parameter from update_one --- mongoengine/queryset/queryset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index bfb5a486..1739f05e 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -469,7 +469,8 @@ class QuerySet(object): .. versionadded:: 0.2 """ - return self.update(upsert=upsert, multi=False, write_concern=None, **update) + return self.update( + upsert=upsert, multi=False, write_concern=write_concern, **update) def with_id(self, object_id): """Retrieve the object matching the id provided. Uses `object_id` only From e58b3390aa8855659c006a8758fa23c075cdcb68 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 1 May 2013 08:48:14 +0000 Subject: [PATCH 16/40] Removed import with from future --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/document.py | 1 - tests/document/class_methods.py | 1 - tests/document/indexes.py | 1 - tests/document/instance.py | 1 - tests/fields/fields.py | 1 - tests/fields/file_tests.py | 1 - tests/queryset/queryset.py | 1 - tests/queryset/transform.py | 1 - tests/queryset/visitor.py | 1 - tests/test_connection.py | 1 - tests/test_context_managers.py | 1 - tests/test_dereference.py | 1 - tests/test_django.py | 1 - 15 files changed, 2 insertions(+), 13 deletions(-) diff --git a/AUTHORS b/AUTHORS index 44e19bf6..181ad5a3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -157,3 +157,4 @@ that much better: * Kenneth Falck * Lukasz Balcerzak * Nicolas Cortot + * Alex (https://github.com/kelsta) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6aa62148..314967e9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.X ================ +- Fixed update_one write concern (#302) - Updated minimum requirement for pymongo to 2.5 - Add support for new geojson fields, indexes and queries (#299) - If values cant be compared mark as changed (#287) diff --git a/mongoengine/document.py b/mongoengine/document.py index 143802cc..0e9be56a 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,4 +1,3 @@ -from __future__ import with_statement import warnings import pymongo diff --git a/tests/document/class_methods.py b/tests/document/class_methods.py index 83e68ff8..231dd8fd 100644 --- a/tests/document/class_methods.py +++ b/tests/document/class_methods.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement import sys sys.path[0:0] = [""] import unittest diff --git a/tests/document/indexes.py b/tests/document/indexes.py index ddc147b3..04d56324 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement import unittest import sys sys.path[0:0] = [""] diff --git a/tests/document/instance.py b/tests/document/instance.py index 06744ab4..d8df0b2d 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement import sys sys.path[0:0] = [""] diff --git a/tests/fields/fields.py b/tests/fields/fields.py index f7ab63ea..30471566 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement import sys sys.path[0:0] = [""] diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index c5842d81..52bd88ab 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement import sys sys.path[0:0] = [""] diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 40aef7ec..dfaae850 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1,4 +1,3 @@ -from __future__ import with_statement import sys sys.path[0:0] = [""] diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index bde4b6f1..7886965b 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -1,4 +1,3 @@ -from __future__ import with_statement import sys sys.path[0:0] = [""] diff --git a/tests/queryset/visitor.py b/tests/queryset/visitor.py index bd81a654..2e9195ec 100644 --- a/tests/queryset/visitor.py +++ b/tests/queryset/visitor.py @@ -1,4 +1,3 @@ -from __future__ import with_statement import sys sys.path[0:0] = [""] diff --git a/tests/test_connection.py b/tests/test_connection.py index 4b8a3d11..d7926489 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,4 +1,3 @@ -from __future__ import with_statement import sys sys.path[0:0] = [""] import unittest diff --git a/tests/test_context_managers.py b/tests/test_context_managers.py index eef63bee..f87d6383 100644 --- a/tests/test_context_managers.py +++ b/tests/test_context_managers.py @@ -1,4 +1,3 @@ -from __future__ import with_statement import sys sys.path[0:0] = [""] import unittest diff --git a/tests/test_dereference.py b/tests/test_dereference.py index ef5a10d9..e146963f 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement import sys sys.path[0:0] = [""] import unittest diff --git a/tests/test_django.py b/tests/test_django.py index 573c0728..e30fe3c9 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1,4 +1,3 @@ -from __future__ import with_statement import sys sys.path[0:0] = [""] import unittest From 9654fe0d8d4657ed24b98bb1396faea77813b290 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 1 May 2013 09:30:20 +0000 Subject: [PATCH 17/40] 0.8.0RC1 is a go! --- docs/changelog.rst | 2 +- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 314967e9..61409253 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,7 +2,7 @@ Changelog ========= -Changes in 0.8.X +Changes in 0.8.0 ================ - Fixed update_one write concern (#302) - Updated minimum requirement for pymongo to 2.5 diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 6fe6d088..0a3ca247 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 0, '+') +VERSION = (0, 8, 0, 'RC1') def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index eaf478dc..33ea48c6 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.7.10 +Version: 0.8.0.RC1 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From cd73654683a2c68d951e7a9c33310fc9fc1ab211 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 1 May 2013 09:48:58 +0000 Subject: [PATCH 18/40] Update readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5eab5021..ea4b5050 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ setup.py install``. Dependencies ============ -- pymongo 2.1.1+ +- pymongo 2.5+ - sphinx (optional - for documentation generation) Examples From 8c9afbd278eac13afdb9e12e23ec0e324d56d539 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 1 May 2013 19:40:49 +0000 Subject: [PATCH 19/40] Fix cloning of sliced querysets (#303) --- mongoengine/queryset/queryset.py | 14 +++----- tests/test_django.py | 60 +++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 1739f05e..c1c93781 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -72,7 +72,6 @@ class QuerySet(object): self._cursor_obj = None self._limit = None self._skip = None - self._slice = None self._hint = -1 # Using -1 as None is a valid value for hint def __call__(self, q_obj=None, class_check=True, slave_okay=False, @@ -127,8 +126,10 @@ class QuerySet(object): if isinstance(key, slice): try: queryset._cursor_obj = queryset._cursor[key] - queryset._slice = key queryset._skip, queryset._limit = key.start, key.stop + queryset._limit + if key.start and key.stop: + queryset._limit = key.stop - key.start except IndexError, err: # PyMongo raises an error if key.start == key.stop, catch it, # bin it, kill it. @@ -537,15 +538,9 @@ class QuerySet(object): val = getattr(self, prop) setattr(c, prop, copy.copy(val)) - if self._slice: - c._slice = self._slice - if self._cursor_obj: c._cursor_obj = self._cursor_obj.clone() - if self._slice: - c._cursor[self._slice] - return c def select_related(self, max_depth=1): @@ -571,7 +566,6 @@ class QuerySet(object): else: queryset._cursor.limit(n) queryset._limit = n - # Return self to allow chaining return queryset @@ -1155,7 +1149,7 @@ class QuerySet(object): self._cursor_obj.sort(order) if self._limit is not None: - self._cursor_obj.limit(self._limit - (self._skip or 0)) + self._cursor_obj.limit(self._limit) if self._skip is not None: self._cursor_obj.skip(self._skip) diff --git a/tests/test_django.py b/tests/test_django.py index e30fe3c9..f81213c3 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -150,22 +150,74 @@ class QuerySetTest(unittest.TestCase): # Try iterating the same queryset twice, nested, in a Django template. names = ['A', 'B', 'C', 'D'] - class User(Document): + class CustomUser(Document): name = StringField() def __unicode__(self): return self.name - User.drop_collection() + CustomUser.drop_collection() for name in names: - User(name=name).save() + CustomUser(name=name).save() - users = User.objects.all().order_by('name') + users = CustomUser.objects.all().order_by('name') template = Template("{% for user in users %}{{ user.name }}{% ifequal forloop.counter 2 %} {% for inner_user in users %}{{ inner_user.name }}{% endfor %} {% endifequal %}{% endfor %}") rendered = template.render(Context({'users': users})) self.assertEqual(rendered, 'AB ABCD CD') + def test_filter(self): + """Ensure that a queryset and filters work as expected + """ + + class Note(Document): + text = StringField() + + for i in xrange(1, 101): + Note(name="Note: %s" % i).save() + + # Check the count + self.assertEqual(Note.objects.count(), 100) + + # Get the first 10 and confirm + notes = Note.objects[:10] + self.assertEqual(notes.count(), 10) + + # Test djangos template filters + # self.assertEqual(length(notes), 10) + t = Template("{{ notes.count }}") + c = Context({"notes": notes}) + self.assertEqual(t.render(c), "10") + + # Test with skip + notes = Note.objects.skip(90) + self.assertEqual(notes.count(), 10) + + # Test djangos template filters + self.assertEqual(notes.count(), 10) + t = Template("{{ notes.count }}") + c = Context({"notes": notes}) + self.assertEqual(t.render(c), "10") + + # Test with limit + notes = Note.objects.skip(90) + self.assertEqual(notes.count(), 10) + + # Test djangos template filters + self.assertEqual(notes.count(), 10) + t = Template("{{ notes.count }}") + c = Context({"notes": notes}) + self.assertEqual(t.render(c), "10") + + # Test with skip and limit + notes = Note.objects.skip(10).limit(10) + + # Test djangos template filters + self.assertEqual(notes.count(), 10) + t = Template("{{ notes.count }}") + c = Context({"notes": notes}) + self.assertEqual(t.render(c), "10") + class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase): backend = SessionStore From 1cdbade7619f887c017905d42cec374ee012d8ff Mon Sep 17 00:00:00 2001 From: Jin Date: Wed, 1 May 2013 16:54:48 -0700 Subject: [PATCH 20/40] fixed typo in defining-documents.rst --- docs/guide/defining-documents.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 2c744b71..0ee5ad3e 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -493,7 +493,7 @@ Compound Indexes and Indexing sub documents Compound indexes can be created by adding the Embedded field or dictionary field name to the index definition. -Sometimes its more efficient to index parts of Embeedded / dictionary fields, +Sometimes its more efficient to index parts of Embedded / dictionary fields, in this case use 'dot' notation to identify the value to index eg: `rank.title` Geospatial indexes From 3002e79c9844973d545a1f9560b88bff0cee4b71 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 2 May 2013 07:35:33 +0000 Subject: [PATCH 21/40] Updated changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 61409253..edadbd76 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.0 ================ +- Fix cloning of sliced querysets (#303) - Fixed update_one write concern (#302) - Updated minimum requirement for pymongo to 2.5 - Add support for new geojson fields, indexes and queries (#299) From 268dd80cd09ddcc4be8df8547d0ccc6eae8a5618 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 2 May 2013 07:35:44 +0000 Subject: [PATCH 22/40] Added Jin Zhang to authors (#304) --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 181ad5a3..0ff48e80 100644 --- a/AUTHORS +++ b/AUTHORS @@ -158,3 +158,5 @@ that much better: * Lukasz Balcerzak * Nicolas Cortot * Alex (https://github.com/kelsta) + * Jin Zhang + From 4a71c5b4249610a16752046ae9268207c3d272a9 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 2 May 2013 10:47:37 +0000 Subject: [PATCH 23/40] Updates to CONTRIBUTING.rst --- CONTRIBUTING.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9688339b..8754896a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -20,7 +20,7 @@ post to the `user group ` Supported Interpreters ---------------------- -PyMongo supports CPython 2.5 and newer. Language +MongoEngine supports CPython 2.6 and newer. Language features not supported by all interpreters can not be used. Please also ensure that your code is properly converted by `2to3 `_ for Python 3 support. @@ -46,7 +46,7 @@ General Guidelines - Write tests and make sure they pass (make sure you have a mongod running on the default port, then execute ``python setup.py test`` from the cmd line to run the test suite). -- Add yourself to AUTHORS.rst :) +- Add yourself to AUTHORS :) Documentation ------------- From a2c429a4a5029358ec9d38d64d53a19f532906ed Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 2 May 2013 10:48:09 +0000 Subject: [PATCH 24/40] Queryset cursor regeneration testcase --- tests/queryset/queryset.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 13039f24..bbb28bde 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -115,6 +115,15 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(len(people), 1) self.assertEqual(people[0].name, 'User B') + # Test slice limit and skip cursor reset + qs = self.Person.objects[1:2] + # fetch then delete the cursor + qs._cursor + qs._cursor_obj = None + people = list(qs) + self.assertEqual(len(people), 1) + self.assertEqual(people[0].name, 'User B') + people = list(self.Person.objects[1:1]) self.assertEqual(len(people), 0) From f2c16452c66c064b466301fa0da1df7cd10c3770 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 2 May 2013 10:48:30 +0000 Subject: [PATCH 25/40] Help with backwards compatibility --- mongoengine/base/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongoengine/base/__init__.py b/mongoengine/base/__init__.py index ce119b3a..e8d4b6ad 100644 --- a/mongoengine/base/__init__.py +++ b/mongoengine/base/__init__.py @@ -3,3 +3,6 @@ from mongoengine.base.datastructures import * from mongoengine.base.document import * from mongoengine.base.fields import * from mongoengine.base.metaclasses import * + +# Help with backwards compatibility +from mongoengine.errors import * From 0eda7a5a3c4a82f5160d50a0e4694f96c54c300d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 2 May 2013 10:51:04 +0000 Subject: [PATCH 26/40] 0.8.0RC2 is a go --- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 0a3ca247..b6adcb4d 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 0, 'RC1') +VERSION = (0, 8, 0, 'RC2') def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 33ea48c6..62ec8f87 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.0.RC1 +Version: 0.8.0.RC2 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From 3ccc495c758aaa2112405663fe9c9f6607dcc24d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 3 May 2013 12:56:53 +0000 Subject: [PATCH 27/40] Fixed register_delete_rule inheritance issue --- mongoengine/base/metaclasses.py | 49 +++++++++++++++++---------------- tests/document/class_methods.py | 21 +++++++++++++- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index def8a055..444d9a25 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -140,8 +140,31 @@ class DocumentMetaclass(type): base._subclasses += (_cls,) base._types = base._subclasses # TODO depreciate _types - # Handle delete rules Document, EmbeddedDocument, DictField = cls._import_classes() + + if issubclass(new_class, Document): + new_class._collection = None + + # Add class to the _document_registry + _document_registry[new_class._class_name] = new_class + + # In Python 2, User-defined methods objects have special read-only + # attributes 'im_func' and 'im_self' which contain the function obj + # and class instance object respectively. With Python 3 these special + # attributes have been replaced by __func__ and __self__. The Blinker + # module continues to use im_func and im_self, so the code below + # copies __func__ into im_func and __self__ into im_self for + # classmethod objects in Document derived classes. + if PY3: + for key, val in new_class.__dict__.items(): + if isinstance(val, classmethod): + f = val.__get__(new_class) + if hasattr(f, '__func__') and not hasattr(f, 'im_func'): + f.__dict__.update({'im_func': getattr(f, '__func__')}) + if hasattr(f, '__self__') and not hasattr(f, 'im_self'): + f.__dict__.update({'im_self': getattr(f, '__self__')}) + + # Handle delete rules for field in new_class._fields.itervalues(): f = field f.owner_document = new_class @@ -167,33 +190,11 @@ class DocumentMetaclass(type): field.name, delete_rule) if (field.name and hasattr(Document, field.name) and - EmbeddedDocument not in new_class.mro()): + EmbeddedDocument not in new_class.mro()): msg = ("%s is a document method and not a valid " "field name" % field.name) raise InvalidDocumentError(msg) - if issubclass(new_class, Document): - new_class._collection = None - - # Add class to the _document_registry - _document_registry[new_class._class_name] = new_class - - # In Python 2, User-defined methods objects have special read-only - # attributes 'im_func' and 'im_self' which contain the function obj - # and class instance object respectively. With Python 3 these special - # attributes have been replaced by __func__ and __self__. The Blinker - # module continues to use im_func and im_self, so the code below - # copies __func__ into im_func and __self__ into im_self for - # classmethod objects in Document derived classes. - if PY3: - for key, val in new_class.__dict__.items(): - if isinstance(val, classmethod): - f = val.__get__(new_class) - if hasattr(f, '__func__') and not hasattr(f, 'im_func'): - f.__dict__.update({'im_func': getattr(f, '__func__')}) - if hasattr(f, '__self__') and not hasattr(f, 'im_self'): - f.__dict__.update({'im_self': getattr(f, '__self__')}) - return new_class def add_to_class(self, name, value): diff --git a/tests/document/class_methods.py b/tests/document/class_methods.py index 231dd8fd..b2c72838 100644 --- a/tests/document/class_methods.py +++ b/tests/document/class_methods.py @@ -5,7 +5,7 @@ import unittest from mongoengine import * -from mongoengine.queryset import NULLIFY +from mongoengine.queryset import NULLIFY, PULL from mongoengine.connection import get_db __all__ = ("ClassMethodsTest", ) @@ -85,6 +85,25 @@ class ClassMethodsTest(unittest.TestCase): self.assertEqual(self.Person._meta['delete_rules'], {(Job, 'employee'): NULLIFY}) + def test_register_delete_rule_inherited(self): + + class Vaccine(Document): + name = StringField(required=True) + + meta = {"indexes": ["name"]} + + class Animal(Document): + family = StringField(required=True) + vaccine_made = ListField(ReferenceField("Vaccine", reverse_delete_rule=PULL)) + + meta = {"allow_inheritance": True, "indexes": ["family"]} + + class Cat(Animal): + name = StringField(required=True) + + self.assertEqual(Vaccine._meta['delete_rules'][(Animal, 'vaccine_made')], PULL) + self.assertEqual(Vaccine._meta['delete_rules'][(Cat, 'vaccine_made')], PULL) + def test_collection_naming(self): """Ensure that a collection with a specified name may be used. """ From ebd15616827149f7c7d1d2a710056fbc652d4da5 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 3 May 2013 14:21:36 +0000 Subject: [PATCH 28/40] Updated changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index edadbd76..bfa809cd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.0 ================ +- Fixed register_delete_rule inheritance issue - Fix cloning of sliced querysets (#303) - Fixed update_one write concern (#302) - Updated minimum requirement for pymongo to 2.5 From 2c119dea472a92e3ac9b3e5be35cc90b260ad6fe Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 May 2013 10:34:13 +0000 Subject: [PATCH 29/40] Upserting is the only way to ensure docs are saved correctly (#306) --- docs/changelog.rst | 1 + mongoengine/document.py | 3 +-- tests/document/instance.py | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bfa809cd..205df4e2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.0 ================ +- Upserting is the only way to ensure docs are saved correctly (#306) - Fixed register_delete_rule inheritance issue - Fix cloning of sliced querysets (#303) - Fixed update_one write concern (#302) diff --git a/mongoengine/document.py b/mongoengine/document.py index 0e9be56a..6c1045bc 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -231,7 +231,6 @@ class Document(BaseDocument): return not updated return created - upsert = self._created update_query = {} if updates: @@ -240,7 +239,7 @@ class Document(BaseDocument): update_query["$unset"] = removals if updates or removals: last_error = collection.update(select_dict, update_query, - upsert=upsert, **write_concern) + upsert=True, **write_concern) created = is_new_object(last_error) cascade = (self._meta.get('cascade', True) diff --git a/tests/document/instance.py b/tests/document/instance.py index d8df0b2d..d84d65c8 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -852,6 +852,14 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, None) self.assertEqual(person.age, None) + def test_inserts_if_you_set_the_pk(self): + p1 = self.Person(name='p1', id=bson.ObjectId()).save() + p2 = self.Person(name='p2') + p2.id = bson.ObjectId() + p2.save() + + self.assertEqual(2, self.Person.objects.count()) + def test_can_save_if_not_included(self): class EmbeddedDoc(EmbeddedDocument): From ddd11c7ed21d1be4392c67fa9e6ef137caecff82 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 May 2013 10:57:52 +0000 Subject: [PATCH 30/40] Added offline docs links --- docs/index.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 6358a315..77f965ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,6 +55,14 @@ See the :doc:`changelog` for a full list of changes to MongoEngine and .. note:: Always read and test the `upgrade `_ documentation before putting updates live in production **;)** +Offline Reading +--------------- + +Download the docs in `pdf `_ +or `epub `_ +formats for offline reading. + + .. toctree:: :maxdepth: 1 :numbered: From 52c162a478ad4796d9a9621a7a1eb68529d992d7 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 May 2013 11:01:23 +0000 Subject: [PATCH 31/40] Pep8 --- mongoengine/fields.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 274ad3c7..de9b44fa 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -107,11 +107,11 @@ class URLField(StringField): """ _URL_REGEX = re.compile( - r'^(?:http|ftp)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?' # optional port + r'^(?:http|ftp)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip + r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) def __init__(self, verify_exists=False, url_regex=None, **kwargs): @@ -128,8 +128,7 @@ class URLField(StringField): warnings.warn( "The URLField verify_exists argument has intractable security " "and performance issues. Accordingly, it has been deprecated.", - DeprecationWarning - ) + DeprecationWarning) try: request = urllib2.Request(value) urllib2.urlopen(request) @@ -469,7 +468,7 @@ class ComplexDateTimeField(StringField): def __get__(self, instance, owner): data = super(ComplexDateTimeField, self).__get__(instance, owner) - if data == None: + if data is None: return datetime.datetime.now() if isinstance(data, datetime.datetime): return data @@ -658,15 +657,15 @@ class ListField(ComplexBaseField): """Make sure that a list of valid fields is being used. """ if (not isinstance(value, (list, tuple, QuerySet)) or - isinstance(value, basestring)): + isinstance(value, basestring)): self.error('Only lists and tuples may be used in a list field') super(ListField, self).validate(value) def prepare_query_value(self, op, value): if self.field: if op in ('set', 'unset') and (not isinstance(value, basestring) - and not isinstance(value, BaseDocument) - and hasattr(value, '__iter__')): + and not isinstance(value, BaseDocument) + and hasattr(value, '__iter__')): return [self.field.prepare_query_value(op, v) for v in value] return self.field.prepare_query_value(op, value) return super(ListField, self).prepare_query_value(op, value) @@ -701,7 +700,7 @@ class SortedListField(ListField): value = super(SortedListField, self).to_mongo(value) if self._ordering is not None: return sorted(value, key=itemgetter(self._ordering), - reverse=self._order_reverse) + reverse=self._order_reverse) return sorted(value, reverse=self._order_reverse) @@ -1001,7 +1000,7 @@ class BinaryField(BaseField): if not isinstance(value, (bin_type, txt_type, Binary)): self.error("BinaryField only accepts instances of " "(%s, %s, Binary)" % ( - bin_type.__name__, txt_type.__name__)) + bin_type.__name__, txt_type.__name__)) if self.max_bytes is not None and len(value) > self.max_bytes: self.error('Binary value is too long') @@ -1235,8 +1234,6 @@ class ImageGridFsProxy(GridFSProxy): Insert a image in database applying field properties (size, thumbnail_size) """ - if not self.instance: - import ipdb; ipdb.set_trace(); field = self.instance._fields[self.key] try: @@ -1308,6 +1305,7 @@ class ImageGridFsProxy(GridFSProxy): height=h, format=format, **kwargs) + @property def size(self): """ From 870ff1d4d986077f9e306b7a9098d2b339b4c246 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 May 2013 11:11:55 +0000 Subject: [PATCH 32/40] Added $setOnInsert support for upserts (#308) Upserts now possible with just query parameters (#309) --- docs/changelog.rst | 2 ++ mongoengine/queryset/queryset.py | 2 +- mongoengine/queryset/transform.py | 7 +++++-- tests/queryset/queryset.py | 22 ++++++++++++++++++++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 205df4e2..ad5f6157 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,8 @@ Changelog Changes in 0.8.0 ================ +- Added $setOnInsert support for upserts (#308) +- Upserts now possible with just query parameters (#309) - Upserting is the only way to ensure docs are saved correctly (#306) - Fixed register_delete_rule inheritance issue - Fix cloning of sliced querysets (#303) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index c1c93781..85b683db 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -427,7 +427,7 @@ class QuerySet(object): .. versionadded:: 0.2 """ - if not update: + if not update and not upsert: raise OperationError("No update parameters, would remove data") if not write_concern: diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 96d99040..4062fc1e 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -24,7 +24,8 @@ 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') + 'push_all', 'pull', 'pull_all', 'add_to_set', + 'set_on_insert') def query(_doc_cls=None, _field_operation=False, **query): @@ -163,7 +164,9 @@ def update(_doc_cls=None, **update): if value > 0: value = -value elif op == 'add_to_set': - op = op.replace('_to_set', 'ToSet') + op = 'addToSet' + elif op == 'set_on_insert': + op = "setOnInsert" match = None if parts[-1] in COMPARISON_OPERATORS: diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index bbb28bde..ffb53786 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -296,10 +296,10 @@ class QuerySetTest(unittest.TestCase): author.save(write_concern=write_concern) result = self.Person.objects.update( - set__name='Ross',write_concern={"w": 1}) + set__name='Ross', write_concern={"w": 1}) self.assertEqual(result, 1) result = self.Person.objects.update( - set__name='Ross',write_concern={"w": 0}) + set__name='Ross', write_concern={"w": 0}) self.assertEqual(result, None) result = self.Person.objects.update_one( @@ -536,6 +536,24 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(club.members['John']['gender'], "F") self.assertEqual(club.members['John']['age'], 14) + def test_upsert(self): + self.Person.drop_collection() + + self.Person.objects(pk=ObjectId(), name="Bob", age=30).update(upsert=True) + + bob = self.Person.objects.first() + self.assertEqual("Bob", bob.name) + self.assertEqual(30, bob.age) + + def test_set_on_insert(self): + self.Person.drop_collection() + + self.Person.objects(pk=ObjectId()).update(set__name='Bob', set_on_insert__age=30, upsert=True) + + bob = self.Person.objects.first() + self.assertEqual("Bob", bob.name) + self.assertEqual(30, bob.age) + def test_get_or_create(self): """Ensure that ``get_or_create`` returns one result or creates a new document. From 7cde97973696eb28e513b522e227d65786378739 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 May 2013 11:39:16 +0000 Subject: [PATCH 33/40] Updated fields --- mongoengine/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index de9b44fa..49959983 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -879,7 +879,7 @@ class ReferenceField(BaseField): """Convert a MongoDB-compatible type to a Python type. """ if (not self.dbref and - not isinstance(value, (DBRef, Document, EmbeddedDocument))): + not isinstance(value, (DBRef, Document, EmbeddedDocument))): collection = self.document_type._get_collection_name() value = DBRef(collection, self.document_type.id.to_python(value)) return value From 9dfee83e687a9aef625fbc38bf5dd10b16e463dc Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 May 2013 11:54:47 +0000 Subject: [PATCH 34/40] Fixed querying string versions of ObjectIds issue with ReferenceField (#307) --- docs/changelog.rst | 1 + mongoengine/fields.py | 2 +- mongoengine/queryset/queryset.py | 5 ++-- tests/queryset/queryset.py | 45 +++++++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ad5f6157..842bc7d0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.0 ================ +- Fixed querying string versions of ObjectIds issue with ReferenceField (#307) - Added $setOnInsert support for upserts (#308) - Upserts now possible with just query parameters (#309) - Upserting is the only way to ensure docs are saved correctly (#306) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 49959983..573d9a03 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -854,7 +854,7 @@ class ReferenceField(BaseField): return document.id return document elif not self.dbref and isinstance(document, basestring): - return document + return ObjectId(document) id_field_name = self.document_type._meta['id_field'] id_field = self.document_type._fields[id_field_name] diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 85b683db..191afdd2 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -544,8 +544,9 @@ class QuerySet(object): return c def select_related(self, max_depth=1): - """Handles dereferencing of :class:`~bson.dbref.DBRef` objects to - a maximum depth in order to cut down the number queries to mongodb. + """Handles dereferencing of :class:`~bson.dbref.DBRef` objects or + :class:`~bson.object_id.ObjectId` a maximum depth in order to cut down + the number queries to mongodb. .. versionadded:: 0.5 """ diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index ffb53786..b9db297b 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1263,7 +1263,7 @@ class QuerySetTest(unittest.TestCase): class BlogPost(Document): content = StringField() authors = ListField(ReferenceField(self.Person, - reverse_delete_rule=PULL)) + reverse_delete_rule=PULL)) BlogPost.drop_collection() self.Person.drop_collection() @@ -1321,6 +1321,49 @@ class QuerySetTest(unittest.TestCase): self.Person.objects()[:1].delete() self.assertEqual(1, BlogPost.objects.count()) + + def test_reference_field_find(self): + """Ensure cascading deletion of referring documents from the database. + """ + class BlogPost(Document): + content = StringField() + author = ReferenceField(self.Person) + + BlogPost.drop_collection() + self.Person.drop_collection() + + me = self.Person(name='Test User').save() + BlogPost(content="test 123", author=me).save() + + self.assertEqual(1, BlogPost.objects(author=me).count()) + self.assertEqual(1, BlogPost.objects(author=me.pk).count()) + self.assertEqual(1, BlogPost.objects(author="%s" % me.pk).count()) + + self.assertEqual(1, BlogPost.objects(author__in=[me]).count()) + self.assertEqual(1, BlogPost.objects(author__in=[me.pk]).count()) + self.assertEqual(1, BlogPost.objects(author__in=["%s" % me.pk]).count()) + + def test_reference_field_find_dbref(self): + """Ensure cascading deletion of referring documents from the database. + """ + class BlogPost(Document): + content = StringField() + author = ReferenceField(self.Person, dbref=True) + + BlogPost.drop_collection() + self.Person.drop_collection() + + me = self.Person(name='Test User').save() + BlogPost(content="test 123", author=me).save() + + self.assertEqual(1, BlogPost.objects(author=me).count()) + self.assertEqual(1, BlogPost.objects(author=me.pk).count()) + self.assertEqual(1, BlogPost.objects(author="%s" % me.pk).count()) + + self.assertEqual(1, BlogPost.objects(author__in=[me]).count()) + self.assertEqual(1, BlogPost.objects(author__in=[me.pk]).count()) + self.assertEqual(1, BlogPost.objects(author__in=["%s" % me.pk]).count()) + def test_update(self): """Ensure that atomic updates work properly. """ From 9e513e08aeafec19399677e9bd813fedd3d596ed Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 May 2013 11:55:56 +0000 Subject: [PATCH 35/40] Updated RC version --- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index b6adcb4d..3a4d7c94 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 0, 'RC2') +VERSION = (0, 8, 0, 'RC3') def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 62ec8f87..68cb72c8 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.0.RC2 +Version: 0.8.0.RC3 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From 96a964a18332b13e5ba4ecd11ee246cf80a8a8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Thu, 9 May 2013 13:18:58 -0300 Subject: [PATCH 36/40] added .disable_inheritance method for the simple fetch exclusives classes --- mongoengine/queryset/queryset.py | 9 +++++++++ tests/queryset/queryset.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 191afdd2..407bf2fd 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -520,6 +520,15 @@ class QuerySet(object): queryset._none = True return queryset + def disable_inheritance(self): + """ + Disable inheritance query, fetch only objects for the query class + """ + if self._document._meta.get('allow_inheritance') is True: + self._initial_query = {"_cls": self._document._class_name} + + return self + def clone(self): """Creates a copy of the current :class:`~mongoengine.queryset.QuerySet` diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index b9db297b..49ed36c5 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3272,5 +3272,25 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(outer_count, 7) # outer loop should be executed seven times total self.assertEqual(inner_total_count, 7 * 7) # inner loop should be executed fourtynine times total + def test_disable_inheritance_queryset(self): + class A(Document): + x = IntField() + y = IntField() + + meta = {'allow_inheritance': True} + + class B(A): + z = IntField() + + A.drop_collection() + + A(x=10, y=20).save() + A(x=15, y=30).save() + B(x=20, y=40).save() + B(x=30, y=50).save() + + for obj in A.objects.disable_inheritance(): + self.assertEqual(obj.__class__, A) + if __name__ == '__main__': unittest.main() From 9251ce312bc0545d3b86224e35a913029a86695e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 10 May 2013 13:57:32 +0000 Subject: [PATCH 37/40] Querysets now utilises a local cache Changed __len__ behavour in the queryset (#247, #311) --- docs/changelog.rst | 3 +- docs/upgrade.rst | 13 ++-- mongoengine/queryset/queryset.py | 111 ++++++++++++++++++++++--------- setup.py | 4 +- tests/queryset/queryset.py | 82 ++++++++++++++++++----- tests/test_jinja.py | 47 +++++++++++++ 6 files changed, 204 insertions(+), 56 deletions(-) create mode 100644 tests/test_jinja.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 842bc7d0..3b6813a0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,8 @@ Changelog Changes in 0.8.0 ================ +- Querysets now utilises a local cache +- Changed __len__ behavour in the queryset (#247, #311) - Fixed querying string versions of ObjectIds issue with ReferenceField (#307) - Added $setOnInsert support for upserts (#308) - Upserts now possible with just query parameters (#309) @@ -25,7 +27,6 @@ Changes in 0.8.0 - Added SequenceField.set_next_value(value) helper (#159) - Updated .only() behaviour - now like exclude it is chainable (#202) - Added with_limit_and_skip support to count() (#235) -- Removed __len__ from queryset (#247) - Objects queryset manager now inherited (#256) - Updated connection to use MongoClient (#262, #274) - Fixed db_alias and inherited Documents (#143) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index c633c28d..fe9e4fa9 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -235,12 +235,15 @@ update your code like so: :: mammals = Animal.objects(type="mammal").filter(order="Carnivora") # The final queryset is assgined to mammals [m for m in mammals] # This will return all carnivores -No more len ------------ +Len iterates the queryset +-------------------------- -If you ever did len(queryset) it previously did a count() under the covers, this -caused some unusual issues - so now it has been removed in favour of the -explicit `queryset.count()` to update:: +If you ever did `len(queryset)` it previously did a `count()` under the covers, +this caused some unusual issues. As `len(queryset)` is most often used by +`list(queryset)` we now cache the queryset results and use that for the length. + +This isn't as performant as a `count()` and if you aren't iterating the +queryset you should upgrade to use count:: # Old code len(Animal.objects(type="mammal")) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 191afdd2..2d631831 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -26,6 +26,7 @@ __all__ = ('QuerySet', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL') # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 +ITER_CHUNK_SIZE = 100 # Delete rules DO_NOTHING = 0 @@ -63,6 +64,9 @@ class QuerySet(object): self._none = False self._as_pymongo = False self._as_pymongo_coerce = False + self._result_cache = [] + self._has_more = True + self._len = None # If inheritance is allowed, only return instances and instances of # subclasses of the class being used @@ -109,13 +113,60 @@ class QuerySet(object): queryset._class_check = class_check return queryset + def __len__(self): + """Since __len__ is called quite frequently (for example, as part of + list(qs) we populate the result cache and cache the length. + """ + if self._len is not None: + return self._len + if self._has_more: + # populate the cache + list(self._iter_results()) + + self._len = len(self._result_cache) + return self._len + def __iter__(self): - """Support iterator protocol""" - queryset = self - if queryset._iter: - queryset = self.clone() - queryset.rewind() - return queryset + """Iteration utilises a results cache which iterates the cursor + in batches of ``ITER_CHUNK_SIZE``. + + If ``self._has_more`` the cursor hasn't been exhausted so cache then + batch. Otherwise iterate the result_cache. + """ + self._iter = True + if self._has_more: + return self._iter_results() + + # iterating over the cache. + return iter(self._result_cache) + + def _iter_results(self): + """A generator for iterating over the result cache. + + Also populates the cache if there are more possible results to yield. + Raises StopIteration when there are no more results""" + pos = 0 + while True: + upper = len(self._result_cache) + while pos < upper: + yield self._result_cache[pos] + pos = pos + 1 + if not self._has_more: + raise StopIteration + if len(self._result_cache) <= pos: + self._populate_cache() + + def _populate_cache(self): + """ + Populates the result cache with ``ITER_CHUNK_SIZE`` more entries + (until the cursor is exhausted). + """ + if self._has_more: + try: + for i in xrange(ITER_CHUNK_SIZE): + self._result_cache.append(self.next()) + except StopIteration: + self._has_more = False def __getitem__(self, key): """Support skip and limit using getitem and slicing syntax. @@ -157,22 +208,15 @@ class QuerySet(object): def __repr__(self): """Provides the string representation of the QuerySet - - .. versionchanged:: 0.6.13 Now doesnt modify the cursor """ + if self._iter: return '.. queryset mid-iteration ..' - data = [] - for i in xrange(REPR_OUTPUT_SIZE + 1): - try: - data.append(self.next()) - except StopIteration: - break + self._populate_cache() + data = self._result_cache[:REPR_OUTPUT_SIZE + 1] if len(data) > REPR_OUTPUT_SIZE: data[-1] = "...(remaining elements truncated)..." - - self.rewind() return repr(data) # Core functions @@ -201,7 +245,7 @@ class QuerySet(object): result = queryset.next() except StopIteration: msg = ("%s matching query does not exist." - % queryset._document._class_name) + % queryset._document._class_name) raise queryset._document.DoesNotExist(msg) try: queryset.next() @@ -352,7 +396,12 @@ class QuerySet(object): """ if self._limit == 0: return 0 - return self._cursor.count(with_limit_and_skip=with_limit_and_skip) + if with_limit_and_skip and self._len is not None: + return self._len + count = self._cursor.count(with_limit_and_skip=with_limit_and_skip) + if with_limit_and_skip: + self._len = count + return count def delete(self, write_concern=None): """Delete the documents matched by the query. @@ -910,7 +959,7 @@ class QuerySet(object): mr_args['out'] = output results = getattr(queryset._collection, map_reduce_function)( - map_f, reduce_f, **mr_args) + map_f, reduce_f, **mr_args) if map_reduce_function == 'map_reduce': results = results.find() @@ -1084,20 +1133,18 @@ class QuerySet(object): def next(self): """Wrap the result in a :class:`~mongoengine.Document` object. """ - self._iter = True - try: - if self._limit == 0 or self._none: - raise StopIteration - if self._scalar: - return self._get_scalar(self._document._from_son( - self._cursor.next())) - if self._as_pymongo: - return self._get_as_pymongo(self._cursor.next()) + if self._limit == 0 or self._none: + raise StopIteration - return self._document._from_son(self._cursor.next()) - except StopIteration, e: - self.rewind() - raise e + raw_doc = self._cursor.next() + if self._as_pymongo: + return self._get_as_pymongo(raw_doc) + + doc = self._document._from_son(raw_doc) + if self._scalar: + return self._get_scalar(doc) + + return doc def rewind(self): """Rewind the cursor to its unevaluated state. diff --git a/setup.py b/setup.py index 10a6dbcb..594f7f81 100644 --- a/setup.py +++ b/setup.py @@ -51,13 +51,13 @@ CLASSIFIERS = [ extra_opts = {} if sys.version_info[0] == 3: extra_opts['use_2to3'] = True - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2'] extra_opts['packages'] = find_packages(exclude=('tests',)) if "test" in sys.argv or "nosetests" in sys.argv: extra_opts['packages'].append("tests") extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]} else: - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'PIL'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'PIL', 'jinja2'] extra_opts['packages'] = find_packages(exclude=('tests',)) setup(name='mongoengine', diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index b9db297b..b9c13963 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -793,7 +793,7 @@ class QuerySetTest(unittest.TestCase): p = p.snapshot(True).slave_okay(True).timeout(True) self.assertEqual(p._cursor_args, - {'snapshot': True, 'slave_okay': True, 'timeout': True}) + {'snapshot': True, 'slave_okay': True, 'timeout': True}) def test_repeated_iteration(self): """Ensure that QuerySet rewinds itself one iteration finishes. @@ -835,6 +835,7 @@ class QuerySetTest(unittest.TestCase): self.assertTrue("Doc: 0" in docs_string) self.assertEqual(docs.count(), 1000) + self.assertTrue('(remaining elements truncated)' in "%s" % docs) # Limit and skip docs = docs[1:4] @@ -3231,6 +3232,51 @@ class QuerySetTest(unittest.TestCase): Organization)) self.assertTrue(isinstance(qs.first().organization, Organization)) + def test_cached_queryset(self): + class Person(Document): + name = StringField() + + Person.drop_collection() + for i in xrange(100): + Person(name="No: %s" % i).save() + + with query_counter() as q: + self.assertEqual(q, 0) + people = Person.objects + + [x for x in people] + self.assertEqual(100, len(people._result_cache)) + self.assertEqual(None, people._len) + self.assertEqual(q, 1) + + list(people) + self.assertEqual(100, people._len) # Caused by list calling len + self.assertEqual(q, 1) + + people.count() # count is cached + self.assertEqual(q, 1) + + def test_cache_not_cloned(self): + + class User(Document): + name = StringField() + + def __unicode__(self): + return self.name + + User.drop_collection() + + User(name="Alice").save() + User(name="Bob").save() + + users = User.objects.all().order_by('name') + self.assertEqual("%s" % users, "[, ]") + self.assertEqual(2, len(users._result_cache)) + + users = users.filter(name="Bob") + self.assertEqual("%s" % users, "[]") + self.assertEqual(1, len(users._result_cache)) + def test_nested_queryset_iterator(self): # Try iterating the same queryset twice, nested. names = ['Alice', 'Bob', 'Chuck', 'David', 'Eric', 'Francis', 'George'] @@ -3247,30 +3293,34 @@ class QuerySetTest(unittest.TestCase): User(name=name).save() users = User.objects.all().order_by('name') - outer_count = 0 inner_count = 0 inner_total_count = 0 - self.assertEqual(users.count(), 7) + with query_counter() as q: + self.assertEqual(q, 0) - for i, outer_user in enumerate(users): - self.assertEqual(outer_user.name, names[i]) - outer_count += 1 - inner_count = 0 - - # Calling len might disrupt the inner loop if there are bugs self.assertEqual(users.count(), 7) - for j, inner_user in enumerate(users): - self.assertEqual(inner_user.name, names[j]) - inner_count += 1 - inner_total_count += 1 + for i, outer_user in enumerate(users): + self.assertEqual(outer_user.name, names[i]) + outer_count += 1 + inner_count = 0 - self.assertEqual(inner_count, 7) # inner loop should always be executed seven times + # Calling len might disrupt the inner loop if there are bugs + self.assertEqual(users.count(), 7) - self.assertEqual(outer_count, 7) # outer loop should be executed seven times total - self.assertEqual(inner_total_count, 7 * 7) # inner loop should be executed fourtynine times total + for j, inner_user in enumerate(users): + self.assertEqual(inner_user.name, names[j]) + inner_count += 1 + inner_total_count += 1 + + self.assertEqual(inner_count, 7) # inner loop should always be executed seven times + + self.assertEqual(outer_count, 7) # outer loop should be executed seven times total + self.assertEqual(inner_total_count, 7 * 7) # inner loop should be executed fourtynine times total + + self.assertEqual(q, 2) if __name__ == '__main__': unittest.main() diff --git a/tests/test_jinja.py b/tests/test_jinja.py new file mode 100644 index 00000000..0449f868 --- /dev/null +++ b/tests/test_jinja.py @@ -0,0 +1,47 @@ +import sys +sys.path[0:0] = [""] + +import unittest + +from mongoengine import * + +import jinja2 + + +class TemplateFilterTest(unittest.TestCase): + + def setUp(self): + connect(db='mongoenginetest') + + def test_jinja2(self): + env = jinja2.Environment() + + class TestData(Document): + title = StringField() + description = StringField() + + TestData.drop_collection() + + examples = [('A', '1'), + ('B', '2'), + ('C', '3')] + + for title, description in examples: + TestData(title=title, description=description).save() + + tmpl = """ +{%- for record in content -%} + {%- if loop.first -%}{ {%- endif -%} + "{{ record.title }}": "{{ record.description }}" + {%- if loop.last -%} }{%- else -%},{% endif -%} +{%- endfor -%} +""" + ctx = {'content': TestData.objects} + template = env.from_string(tmpl) + rendered = template.render(**ctx) + + self.assertEqual('{"A": "1","B": "2","C": "3"}', rendered) + + +if __name__ == '__main__': + unittest.main() From 5b498bd8d6f161e81605a686fe927307b2e28078 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 10 May 2013 15:05:16 +0000 Subject: [PATCH 38/40] Added no_sub_classes context manager and queryset helper (#312) --- docs/changelog.rst | 1 + mongoengine/context_managers.py | 36 ++++++++++++++++++++-- mongoengine/queryset/queryset.py | 4 +-- tests/queryset/queryset.py | 23 ++++++++++++-- tests/test_context_managers.py | 51 +++++++++++++++++++++++++++++++- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3b6813a0..c3e50e40 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.0 ================ +- Added no_sub_classes context manager and queryset helper (#312) - Querysets now utilises a local cache - Changed __len__ behavour in the queryset (#247, #311) - Fixed querying string versions of ObjectIds issue with ReferenceField (#307) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index 76d5fbfa..1280e117 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -1,8 +1,10 @@ from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db -from mongoengine.queryset import OperationError, QuerySet +from mongoengine.queryset import QuerySet -__all__ = ("switch_db", "switch_collection", "no_dereference", "query_counter") + +__all__ = ("switch_db", "switch_collection", "no_dereference", + "no_sub_classes", "query_counter") class switch_db(object): @@ -130,6 +132,36 @@ class no_dereference(object): return self.cls +class no_sub_classes(object): + """ no_sub_classes context manager. + + Only returns instances of this class and no sub (inherited) classes:: + + with no_sub_classes(Group) as Group: + Group.objects.find() + + """ + + def __init__(self, cls): + """ Construct the no_sub_classes context manager. + + :param cls: the class to turn querying sub classes on + """ + self.cls = cls + + def __enter__(self): + """ change the objects default and _auto_dereference values""" + self.cls._all_subclasses = self.cls._subclasses + self.cls._subclasses = (self.cls,) + return self.cls + + def __exit__(self, t, value, traceback): + """ Reset the default and _auto_dereference values""" + self.cls._subclasses = self.cls._all_subclasses + delattr(self.cls, '_all_subclasses') + return self.cls + + class QuerySetNoDeRef(QuerySet): """Special no_dereference QuerySet""" def __dereference(items, max_depth=1, instance=None, name=None): diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 4cf86d1e..5da62953 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -569,9 +569,9 @@ class QuerySet(object): queryset._none = True return queryset - def disable_inheritance(self): + def no_sub_classes(self): """ - Disable inheritance query, fetch only objects for the query class + Only return instances of this document and not any inherited documents """ if self._document._meta.get('allow_inheritance') is True: self._initial_query = {"_cls": self._document._class_name} diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 27a418dc..bf237618 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3322,7 +3322,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(q, 2) - def test_disable_inheritance_queryset(self): + def test_no_sub_classes(self): class A(Document): x = IntField() y = IntField() @@ -3332,15 +3332,34 @@ class QuerySetTest(unittest.TestCase): class B(A): z = IntField() + class C(B): + zz = IntField() + A.drop_collection() A(x=10, y=20).save() A(x=15, y=30).save() B(x=20, y=40).save() B(x=30, y=50).save() + C(x=40, y=60).save() - for obj in A.objects.disable_inheritance(): + self.assertEqual(A.objects.no_sub_classes().count(), 2) + self.assertEqual(A.objects.count(), 5) + + self.assertEqual(B.objects.no_sub_classes().count(), 2) + self.assertEqual(B.objects.count(), 3) + + self.assertEqual(C.objects.no_sub_classes().count(), 1) + self.assertEqual(C.objects.count(), 1) + + for obj in A.objects.no_sub_classes(): self.assertEqual(obj.__class__, A) + for obj in B.objects.no_sub_classes(): + self.assertEqual(obj.__class__, B) + + for obj in C.objects.no_sub_classes(): + self.assertEqual(obj.__class__, C) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_context_managers.py b/tests/test_context_managers.py index f87d6383..c201a5fc 100644 --- a/tests/test_context_managers.py +++ b/tests/test_context_managers.py @@ -5,7 +5,8 @@ import unittest from mongoengine import * from mongoengine.connection import get_db from mongoengine.context_managers import (switch_db, switch_collection, - no_dereference, query_counter) + no_sub_classes, no_dereference, + query_counter) class ContextManagersTest(unittest.TestCase): @@ -138,6 +139,54 @@ class ContextManagersTest(unittest.TestCase): self.assertTrue(isinstance(group.ref, User)) self.assertTrue(isinstance(group.generic, User)) + def test_no_sub_classes(self): + class A(Document): + x = IntField() + y = IntField() + + meta = {'allow_inheritance': True} + + class B(A): + z = IntField() + + class C(B): + zz = IntField() + + A.drop_collection() + + A(x=10, y=20).save() + A(x=15, y=30).save() + B(x=20, y=40).save() + B(x=30, y=50).save() + C(x=40, y=60).save() + + self.assertEqual(A.objects.count(), 5) + self.assertEqual(B.objects.count(), 3) + self.assertEqual(C.objects.count(), 1) + + with no_sub_classes(A) as A: + self.assertEqual(A.objects.count(), 2) + + for obj in A.objects: + self.assertEqual(obj.__class__, A) + + with no_sub_classes(B) as B: + self.assertEqual(B.objects.count(), 2) + + for obj in B.objects: + self.assertEqual(obj.__class__, B) + + with no_sub_classes(C) as C: + self.assertEqual(C.objects.count(), 1) + + for obj in C.objects: + self.assertEqual(obj.__class__, C) + + # Confirm context manager exit correctly + self.assertEqual(A.objects.count(), 5) + self.assertEqual(B.objects.count(), 3) + self.assertEqual(C.objects.count(), 1) + def test_query_counter(self): connect('mongoenginetest') db = get_db() From f8350409ad57c95c1b89c47c0c331b58bee26be6 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 10 May 2013 15:08:01 +0000 Subject: [PATCH 39/40] assertEquals is bad --- tests/document/instance.py | 20 ++++++++++---------- tests/queryset/queryset.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index d84d65c8..dcb0de32 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -319,8 +319,8 @@ class InstanceTest(unittest.TestCase): Location.drop_collection() - self.assertEquals(Area, get_document("Area")) - self.assertEquals(Area, get_document("Location.Area")) + self.assertEqual(Area, get_document("Area")) + self.assertEqual(Area, get_document("Location.Area")) def test_creation(self): """Ensure that document may be created using keyword arguments. @@ -508,12 +508,12 @@ class InstanceTest(unittest.TestCase): t = TestDocument(status="published") t.save(clean=False) - self.assertEquals(t.pub_date, None) + self.assertEqual(t.pub_date, None) t = TestDocument(status="published") t.save(clean=True) - self.assertEquals(type(t.pub_date), datetime) + self.assertEqual(type(t.pub_date), datetime) def test_document_embedded_clean(self): class TestEmbeddedDocument(EmbeddedDocument): @@ -545,7 +545,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(e.to_dict(), {'doc': {'__all__': expect_msg}}) t = TestDocument(doc=TestEmbeddedDocument(x=10, y=25)).save() - self.assertEquals(t.doc.z, 35) + self.assertEqual(t.doc.z, 35) # Asserts not raises t = TestDocument(doc=TestEmbeddedDocument(x=15, y=35, z=5)) @@ -1903,11 +1903,11 @@ class InstanceTest(unittest.TestCase): A.objects.all() - self.assertEquals('testdb-2', B._meta.get('db_alias')) - self.assertEquals('mongoenginetest', - A._get_collection().database.name) - self.assertEquals('mongoenginetest2', - B._get_collection().database.name) + self.assertEqual('testdb-2', B._meta.get('db_alias')) + self.assertEqual('mongoenginetest', + A._get_collection().database.name) + self.assertEqual('mongoenginetest2', + B._get_collection().database.name) def test_db_alias_propagates(self): """db_alias propagates? diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index bf237618..9e1fda21 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -282,7 +282,7 @@ class QuerySetTest(unittest.TestCase): a_objects = A.objects(s='test1') query = B.objects(ref__in=a_objects) query = query.filter(boolfield=True) - self.assertEquals(query.count(), 1) + self.assertEqual(query.count(), 1) def test_update_write_concern(self): """Test that passing write_concern works""" From b16eabd2b6350fdc5a05036034d7d0175c33d6a7 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 10 May 2013 15:09:08 +0000 Subject: [PATCH 40/40] Updated version --- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 3a4d7c94..0f8913a0 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 0, 'RC3') +VERSION = (0, 8, 0, 'RC4') def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 68cb72c8..be9c67be 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.0.RC3 +Version: 0.8.0.RC4 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB