Compare commits
169 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3da37fbf6e | ||
|
69989365c7 | ||
|
2b9c526b47 | ||
|
d7c42861fb | ||
|
e9d478ed9f | ||
|
d6cb5b9abe | ||
|
5580b003b5 | ||
|
67736c849d | ||
|
39e27735cc | ||
|
0902b95764 | ||
|
dc7181a3fd | ||
|
e93c4c87d8 | ||
|
dcec61e9b2 | ||
|
007f116bfa | ||
|
6817f3b7ba | ||
|
36993029ad | ||
|
012352cf24 | ||
|
26723992e3 | ||
|
3591593ac7 | ||
|
d3c2dfbaee | ||
|
b2b4456f74 | ||
|
f666141981 | ||
|
fb4c4e3e08 | ||
|
34fa5cd241 | ||
|
833fa3d94d | ||
|
92471445ec | ||
|
3acfd90720 | ||
|
4742328b90 | ||
|
b4c54b1b62 | ||
|
76cb851c40 | ||
|
3fcc0e9789 | ||
|
8e65154201 | ||
|
c0f7c4ca2d | ||
|
db2f64c290 | ||
|
a3c46fec07 | ||
|
62388cb740 | ||
|
9c9903664a | ||
|
556eed0151 | ||
|
4012722a8d | ||
|
4c68bc6c96 | ||
|
159923fae2 | ||
|
72c7a010ff | ||
|
2c8f004103 | ||
|
67a9b358a0 | ||
|
b5eb3ea1cd | ||
|
98bc0a7c10 | ||
|
bb24879149 | ||
|
3d6ee0ce00 | ||
|
ee72845701 | ||
|
d158727154 | ||
|
73092dcb33 | ||
|
91ddd310ba | ||
|
20dd7562e0 | ||
|
b7e84031e3 | ||
|
f11ee1f9cf | ||
|
449f5a00dc | ||
|
bd1bf9ba24 | ||
|
2af5f3c56e | ||
|
1849f75ad0 | ||
|
32e66b29f4 | ||
|
69012e8ad1 | ||
|
17642c8a8c | ||
|
f1aec68f23 | ||
|
3e30d71263 | ||
|
266f33adc4 | ||
|
dcc8d22cec | ||
|
9540555b26 | ||
|
185e7a6a7e | ||
|
c39f315ddc | ||
|
5b230b90b9 | ||
|
1ed9a36d0a | ||
|
e0911a5fe0 | ||
|
3297578e8d | ||
|
954d5c16d8 | ||
|
95efa39b52 | ||
|
c27ccc91d2 | ||
|
4fb6fcabef | ||
|
2635e41f69 | ||
|
ba01817ee3 | ||
|
1e1d7073c8 | ||
|
40eb23a97a | ||
|
f4711699e4 | ||
|
3b62cf80cd | ||
|
d99c5973c3 | ||
|
7de9adc6b1 | ||
|
17addbefe2 | ||
|
d274576b47 | ||
|
6373e20696 | ||
|
809fe44b43 | ||
|
198ccc028a | ||
|
b96e27a7e4 | ||
|
1147ac4350 | ||
|
21d267cb11 | ||
|
6791f205af | ||
|
7ab2e21c10 | ||
|
2f991ac6f1 | ||
|
9411b38508 | ||
|
386c48b116 | ||
|
9d82911f63 | ||
|
51065e7a4d | ||
|
327452622e | ||
|
13316e5380 | ||
|
9f98025b8c | ||
|
564f950037 | ||
|
be651caa68 | ||
|
aa00feb6a5 | ||
|
03c0fd9ada | ||
|
6093e88eeb | ||
|
ec519f20fa | ||
|
d3495896fa | ||
|
323c86308a | ||
|
f9057e1a28 | ||
|
9596a25bb9 | ||
|
47bfeec115 | ||
|
6bfd6c322b | ||
|
0512dd4c25 | ||
|
acbc741037 | ||
|
c2163ecee5 | ||
|
71689fcf23 | ||
|
1c334141ee | ||
|
b89d71bfa5 | ||
|
3179c4e4ac | ||
|
f5e39c0064 | ||
|
86e2797c57 | ||
|
39b749432a | ||
|
0ad343484f | ||
|
196606438c | ||
|
6896818bfd | ||
|
eb4f0ad7fb | ||
|
467e61bcc1 | ||
|
a2c78c9063 | ||
|
b23353e376 | ||
|
b8e9790de3 | ||
|
e37e8d9e65 | ||
|
f657432be3 | ||
|
80c2895e56 | ||
|
88da998532 | ||
|
225972e151 | ||
|
4972bdb383 | ||
|
11c7a15067 | ||
|
9df725165b | ||
|
682326c130 | ||
|
86575cb035 | ||
|
eecc6188a7 | ||
|
3b4df4615a | ||
|
edfda6ad5b | ||
|
3c7e8be2e7 | ||
|
416fcba846 | ||
|
e196e229cd | ||
|
da57572409 | ||
|
ef172712da | ||
|
170c56bcb9 | ||
|
f3ca9fa4c5 | ||
|
48facec524 | ||
|
ee0c75a26d | ||
|
e9c92f30ba | ||
|
0a074e52e0 | ||
|
da3f4c30e2 | ||
|
2b08ca7c99 | ||
|
c8e466a160 | ||
|
a39685d98c | ||
|
90200dbe9c | ||
|
2304dac8e3 | ||
|
38b2919c0d | ||
|
207fd9fcb7 | ||
|
fbcf58c48f | ||
|
8f4a579df9 | ||
|
600ca3bcf9 | ||
|
a4d2f22fd2 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,9 @@
|
||||
*.pyc
|
||||
.*.swp
|
||||
*.egg
|
||||
docs/.build
|
||||
docs/_build
|
||||
build/
|
||||
dist/
|
||||
mongoengine.egg-info/
|
||||
mongoengine.egg-info/
|
||||
env/
|
1
AUTHORS
1
AUTHORS
@@ -2,3 +2,4 @@ Harry Marr <harry@hmarr.com>
|
||||
Matt Dennewitz <mattdennewitz@gmail.com>
|
||||
Deepak Thukral <iapain@yahoo.com>
|
||||
Florian Schlachter <flori@n-schlachter.de>
|
||||
Steve Challis <steve@stevechallis.com>
|
||||
|
@@ -64,3 +64,7 @@ Fields
|
||||
.. autoclass:: mongoengine.ReferenceField
|
||||
|
||||
.. autoclass:: mongoengine.GenericReferenceField
|
||||
|
||||
.. autoclass:: mongoengine.FileField
|
||||
|
||||
.. autoclass:: mongoengine.GeoPointField
|
||||
|
@@ -2,6 +2,32 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Changes in v0.4
|
||||
===============
|
||||
- Added ``GridFSStorage`` Django storage backend
|
||||
- Added ``FileField`` for GridFS support
|
||||
- New Q-object implementation, which is no longer based on Javascript
|
||||
- Added ``SortedListField``
|
||||
- Added ``EmailField``
|
||||
- Added ``GeoPointField``
|
||||
- Added ``exact`` and ``iexact`` match operators to ``QuerySet``
|
||||
- Added ``get_document_or_404`` and ``get_list_or_404`` Django shortcuts
|
||||
- Added new query operators for Geo queries
|
||||
- Added ``not`` query operator
|
||||
- Added new update operators: ``pop`` and ``add_to_set``
|
||||
- Added ``__raw__`` query parameter
|
||||
- Added support for custom querysets
|
||||
- Fixed document inheritance primary key issue
|
||||
- Added support for querying by array element position
|
||||
- Base class can now be defined for ``DictField``
|
||||
- Fixed MRO error that occured on document inheritance
|
||||
- Added ``QuerySet.distinct``, ``QuerySet.create``, ``QuerySet.snapshot``,
|
||||
``QuerySet.timeout`` and ``QuerySet.all``
|
||||
- Subsequent calls to ``connect()`` now work
|
||||
- Introduced ``min_length`` for ``StringField``
|
||||
- Fixed multi-process connection issue
|
||||
- Other minor fixes
|
||||
|
||||
Changes in v0.3
|
||||
===============
|
||||
- Added MapReduce support
|
||||
|
@@ -19,7 +19,7 @@ MongoDB but still use many of the Django authentication infrastucture (such as
|
||||
the :func:`login_required` decorator and the :func:`authenticate` function). To
|
||||
enable the MongoEngine auth backend, add the following to you **settings.py**
|
||||
file::
|
||||
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'mongoengine.django.auth.MongoEngineBackend',
|
||||
)
|
||||
@@ -44,3 +44,44 @@ into you settings module::
|
||||
SESSION_ENGINE = 'mongoengine.django.sessions'
|
||||
|
||||
.. versionadded:: 0.2.1
|
||||
|
||||
Storage
|
||||
=======
|
||||
With MongoEngine's support for GridFS via the :class:`~mongoengine.FileField`,
|
||||
it is useful to have a Django file storage backend that wraps this. The new
|
||||
storage module is called :class:`~mongoengine.django.GridFSStorage`. Using it
|
||||
is very similar to using the default FileSystemStorage.::
|
||||
|
||||
fs = mongoengine.django.GridFSStorage()
|
||||
|
||||
filename = fs.save('hello.txt', 'Hello, World!')
|
||||
|
||||
All of the `Django Storage API methods
|
||||
<http://docs.djangoproject.com/en/dev/ref/files/storage/>`_ have been
|
||||
implemented except :func:`path`. If the filename provided already exists, an
|
||||
underscore and a number (before # the file extension, if one exists) will be
|
||||
appended to the filename until the generated filename doesn't exist. The
|
||||
:func:`save` method will return the new filename.::
|
||||
|
||||
>>> fs.exists('hello.txt')
|
||||
True
|
||||
>>> fs.open('hello.txt').read()
|
||||
'Hello, World!'
|
||||
>>> fs.size('hello.txt')
|
||||
13
|
||||
>>> fs.url('hello.txt')
|
||||
'http://your_media_url/hello.txt'
|
||||
>>> fs.open('hello.txt').name
|
||||
'hello.txt'
|
||||
>>> fs.listdir()
|
||||
([], [u'hello.txt'])
|
||||
|
||||
All files will be saved and retrieved in GridFS via the :class::`FileDocument`
|
||||
document, allowing easy access to the files without the GridFSStorage
|
||||
backend.::
|
||||
|
||||
>>> from mongoengine.django.storage import FileDocument
|
||||
>>> FileDocument.objects()
|
||||
[<FileDocument: FileDocument object>]
|
||||
|
||||
.. versionadded:: 0.4
|
||||
|
@@ -22,7 +22,7 @@ objects** as class attributes to the document class::
|
||||
|
||||
class Page(Document):
|
||||
title = StringField(max_length=200, required=True)
|
||||
date_modified = DateTimeField(default=datetime.now)
|
||||
date_modified = DateTimeField(default=datetime.datetime.now)
|
||||
|
||||
Fields
|
||||
======
|
||||
@@ -46,6 +46,65 @@ are as follows:
|
||||
* :class:`~mongoengine.EmbeddedDocumentField`
|
||||
* :class:`~mongoengine.ReferenceField`
|
||||
* :class:`~mongoengine.GenericReferenceField`
|
||||
* :class:`~mongoengine.BooleanField`
|
||||
* :class:`~mongoengine.FileField`
|
||||
* :class:`~mongoengine.EmailField`
|
||||
* :class:`~mongoengine.SortedListField`
|
||||
* :class:`~mongoengine.BinaryField`
|
||||
* :class:`~mongoengine.GeoPointField`
|
||||
|
||||
Field arguments
|
||||
---------------
|
||||
Each field type can be customized by keyword arguments. The following keyword
|
||||
arguments can be set on all fields:
|
||||
|
||||
:attr:`db_field` (Default: None)
|
||||
The MongoDB field name.
|
||||
|
||||
:attr:`name` (Default: None)
|
||||
The mongoengine field name.
|
||||
|
||||
:attr:`required` (Default: False)
|
||||
If set to True and the field is not set on the document instance, a
|
||||
:class:`~mongoengine.base.ValidationError` will be raised when the document is
|
||||
validated.
|
||||
|
||||
:attr:`default` (Default: None)
|
||||
A value to use when no value is set for this field.
|
||||
|
||||
The definion of default parameters follow `the general rules on Python
|
||||
<http://docs.python.org/reference/compound_stmts.html#function-definitions>`__,
|
||||
which means that some care should be taken when dealing with default mutable objects
|
||||
(like in :class:`~mongoengine.ListField` or :class:`~mongoengine.DictField`)::
|
||||
|
||||
class ExampleFirst(Document):
|
||||
# Default an empty list
|
||||
values = ListField(IntField(), default=list)
|
||||
|
||||
class ExampleSecond(Document):
|
||||
# Default a set of values
|
||||
values = ListField(IntField(), default=lambda: [1,2,3])
|
||||
|
||||
class ExampleDangerous(Document):
|
||||
# This can make an .append call to add values to the default (and all the following objects),
|
||||
# instead to just an object
|
||||
values = ListField(IntField(), default=[1,2,3])
|
||||
|
||||
|
||||
:attr:`unique` (Default: False)
|
||||
When True, no documents in the collection will have the same value for this
|
||||
field.
|
||||
|
||||
:attr:`unique_with` (Default: None)
|
||||
A field name (or list of field names) that when taken together with this
|
||||
field, will not have two documents in the collection with the same value.
|
||||
|
||||
:attr:`primary_key` (Default: False)
|
||||
When True, use this field as a primary key for the collection.
|
||||
|
||||
:attr:`choices` (Default: None)
|
||||
An iterable of choices to which the value of this field should be limited.
|
||||
|
||||
|
||||
List fields
|
||||
-----------
|
||||
@@ -178,7 +237,21 @@ either a single field name, or a list or tuple of field names::
|
||||
class User(Document):
|
||||
username = StringField(unique=True)
|
||||
first_name = StringField()
|
||||
last_name = StringField(unique_with='last_name')
|
||||
last_name = StringField(unique_with='first_name')
|
||||
|
||||
Skipping Document validation on save
|
||||
------------------------------------
|
||||
You can also skip the whole document validation process by setting
|
||||
``validate=False`` when caling the :meth:`~mongoengine.document.Document.save`
|
||||
method::
|
||||
|
||||
class Recipient(Document):
|
||||
name = StringField()
|
||||
email = EmailField()
|
||||
|
||||
recipient = Recipient(name='admin', email='root@localhost')
|
||||
recipient.save() # will raise a ValidationError while
|
||||
recipient.save(validate=False) # won't
|
||||
|
||||
Document collections
|
||||
====================
|
||||
@@ -225,6 +298,10 @@ or a **-** sign. Note that direction only matters on multi-field indexes. ::
|
||||
meta = {
|
||||
'indexes': ['title', ('title', '-rating')]
|
||||
}
|
||||
|
||||
.. note::
|
||||
Geospatial indexes will be automatically created for all
|
||||
:class:`~mongoengine.GeoPointField`\ s
|
||||
|
||||
Ordering
|
||||
========
|
||||
|
@@ -59,6 +59,13 @@ you may still use :attr:`id` to access the primary key if you want::
|
||||
>>> bob.id == bob.email == 'bob@example.com'
|
||||
True
|
||||
|
||||
You can also access the document's "primary key" using the :attr:`pk` field; in
|
||||
is an alias to :attr:`id`::
|
||||
|
||||
>>> page = Page(title="Another Test Page")
|
||||
>>> page.save()
|
||||
>>> page.id == page.pk
|
||||
|
||||
.. note::
|
||||
If you define your own primary key field, the field implicitly becomes
|
||||
required, so a :class:`ValidationError` will be thrown if you don't provide
|
||||
|
83
docs/guide/gridfs.rst
Normal file
83
docs/guide/gridfs.rst
Normal file
@@ -0,0 +1,83 @@
|
||||
======
|
||||
GridFS
|
||||
======
|
||||
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Writing
|
||||
-------
|
||||
|
||||
GridFS support comes in the form of the :class:`~mongoengine.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,
|
||||
a document is created to store details about animals, including a photo::
|
||||
|
||||
class Animal(Document):
|
||||
genus = StringField()
|
||||
family = StringField()
|
||||
photo = FileField()
|
||||
|
||||
marmot = Animal('Marmota', 'Sciuridae')
|
||||
|
||||
marmot_photo = open('marmot.jpg', 'r') # Retrieve a photo from disk
|
||||
marmot.photo = marmot_photo # Store photo in the document
|
||||
marmot.photo.content_type = 'image/jpeg' # Store metadata
|
||||
|
||||
marmot.save()
|
||||
|
||||
Another way of writing to a :class:`~mongoengine.FileField` is to use the
|
||||
:func:`put` method. This allows for metadata to be stored in the same call as
|
||||
the file::
|
||||
|
||||
marmot.photo.put(marmot_photo, content_type='image/jpeg')
|
||||
|
||||
marmot.save()
|
||||
|
||||
Retrieval
|
||||
---------
|
||||
|
||||
So using the :class:`~mongoengine.FileField` is just like using any other
|
||||
field. The file can also be retrieved just as easily::
|
||||
|
||||
marmot = Animal.objects(genus='Marmota').first()
|
||||
photo = marmot.photo.read()
|
||||
content_type = marmot.photo.content_type
|
||||
|
||||
Streaming
|
||||
---------
|
||||
|
||||
Streaming data into a :class:`~mongoengine.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`::
|
||||
|
||||
marmot.photo.new_file()
|
||||
marmot.photo.write('some_image_data')
|
||||
marmot.photo.write('some_more_image_data')
|
||||
marmot.photo.close()
|
||||
|
||||
marmot.photo.save()
|
||||
|
||||
Deletion
|
||||
--------
|
||||
|
||||
Deleting stored files is achieved with the :func:`delete` method::
|
||||
|
||||
marmot.photo.delete()
|
||||
|
||||
.. note::
|
||||
The FileField in a Document actually only stores the ID of a file in a
|
||||
separate GridFS collection. This means that deleting a document
|
||||
with a defined FileField does not actually delete the file. You must be
|
||||
careful to delete any files in a Document as above before deleting the
|
||||
Document itself.
|
||||
|
||||
|
||||
Replacing files
|
||||
---------------
|
||||
|
||||
Files can be replaced with the :func:`replace` method. This works just like
|
||||
the :func:`put` method so even metadata can (and should) be replaced::
|
||||
|
||||
another_marmot = open('another_marmot.png', 'r')
|
||||
marmot.photo.replace(another_marmot, content_type='image/png')
|
@@ -10,3 +10,4 @@ User Guide
|
||||
defining-documents
|
||||
document-instances
|
||||
querying
|
||||
gridfs
|
||||
|
@@ -34,7 +34,7 @@ arguments. The keys in the keyword arguments correspond to fields on the
|
||||
Fields on embedded documents may also be referred to using field lookup syntax
|
||||
by using a double-underscore in place of the dot in object attribute access
|
||||
syntax::
|
||||
|
||||
|
||||
# This will return a QuerySet that will only iterate over pages that have
|
||||
# been written by a user whose 'country' field is set to 'uk'
|
||||
uk_pages = Page.objects(author__country='uk')
|
||||
@@ -53,31 +53,45 @@ lists that contain that item will be matched::
|
||||
# 'tags' list
|
||||
Page.objects(tags='coding')
|
||||
|
||||
Raw queries
|
||||
-----------
|
||||
It is possible to provide a raw PyMongo query as a query parameter, which will
|
||||
be integrated directly into the query. This is done using the ``__raw__``
|
||||
keyword argument::
|
||||
|
||||
Page.objects(__raw__={'tags': 'coding'})
|
||||
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Query operators
|
||||
===============
|
||||
Operators other than equality may also be used in queries; just attach the
|
||||
operator name to a key with a double-underscore::
|
||||
|
||||
|
||||
# Only find users whose age is 18 or less
|
||||
young_users = Users.objects(age__lte=18)
|
||||
|
||||
Available operators are as follows:
|
||||
|
||||
* ``neq`` -- not equal to
|
||||
* ``ne`` -- not equal to
|
||||
* ``lt`` -- less than
|
||||
* ``lte`` -- less than or equal to
|
||||
* ``gt`` -- greater than
|
||||
* ``gte`` -- greater than or equal to
|
||||
* ``not`` -- negate a standard check, may be used before other operators (e.g.
|
||||
``Q(age__not__mod=5)``)
|
||||
* ``in`` -- value is in list (a list of values should be provided)
|
||||
* ``nin`` -- value is not in list (a list of values should be provided)
|
||||
* ``mod`` -- ``value % x == y``, where ``x`` and ``y`` are two provided values
|
||||
* ``all`` -- every item in array is in list of values provided
|
||||
* ``all`` -- every item in list of values provided is in array
|
||||
* ``size`` -- the size of the array is
|
||||
* ``exists`` -- value for field exists
|
||||
|
||||
The following operators are available as shortcuts to querying with regular
|
||||
expressions:
|
||||
|
||||
* ``exact`` -- string field exactly matches value
|
||||
* ``iexact`` -- string field exactly matches value (case insensitive)
|
||||
* ``contains`` -- string field contains value
|
||||
* ``icontains`` -- string field contains value (case insensitive)
|
||||
* ``startswith`` -- string field starts with value
|
||||
@@ -87,6 +101,27 @@ expressions:
|
||||
|
||||
.. versionadded:: 0.3
|
||||
|
||||
There are a few special operators for performing geographical queries, that
|
||||
may used with :class:`~mongoengine.GeoPointField`\ s:
|
||||
|
||||
* ``within_distance`` -- provide a list containing a point and a maximum
|
||||
distance (e.g. [(41.342, -87.653), 5])
|
||||
* ``within_box`` -- filter documents to those within a given bounding box (e.g.
|
||||
[(35.0, -125.0), (40.0, -100.0)])
|
||||
* ``near`` -- order the documents by how close they are to a given point
|
||||
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Querying by position
|
||||
====================
|
||||
It is possible to query by position in a list by using a numerical value as a
|
||||
query operator. So if you wanted to find all pages whose first tag was ``db``,
|
||||
you could use the following query::
|
||||
|
||||
BlogPost.objects(tags__0='db')
|
||||
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Limiting and skipping results
|
||||
=============================
|
||||
Just as with traditional ORMs, you may limit the number of results returned, or
|
||||
@@ -109,7 +144,7 @@ You may also index the query to retrieve a single result. If an item at that
|
||||
index does not exists, an :class:`IndexError` will be raised. A shortcut for
|
||||
retrieving the first result and returning :attr:`None` if no result exists is
|
||||
provided (:meth:`~mongoengine.queryset.QuerySet.first`)::
|
||||
|
||||
|
||||
>>> # Make sure there are no users
|
||||
>>> User.drop_collection()
|
||||
>>> User.objects[0]
|
||||
@@ -135,8 +170,8 @@ additional keyword argument, :attr:`defaults` may be provided, which will be
|
||||
used as default values for the new document, in the case that it should need
|
||||
to be created::
|
||||
|
||||
>>> a = User.objects.get_or_create(name='User A', defaults={'age': 30})
|
||||
>>> b = User.objects.get_or_create(name='User A', defaults={'age': 40})
|
||||
>>> a, created = User.objects.get_or_create(name='User A', defaults={'age': 30})
|
||||
>>> b, created = User.objects.get_or_create(name='User A', defaults={'age': 40})
|
||||
>>> a.name == b.name and a.age == b.age
|
||||
True
|
||||
|
||||
@@ -172,13 +207,29 @@ custom manager methods as you like::
|
||||
|
||||
@queryset_manager
|
||||
def live_posts(doc_cls, queryset):
|
||||
return queryset.order_by('-date')
|
||||
return queryset.filter(published=True)
|
||||
|
||||
BlogPost(title='test1', published=False).save()
|
||||
BlogPost(title='test2', published=True).save()
|
||||
assert len(BlogPost.objects) == 2
|
||||
assert len(BlogPost.live_posts) == 1
|
||||
|
||||
Custom QuerySets
|
||||
================
|
||||
Should you want to add custom methods for interacting with or filtering
|
||||
documents, extending the :class:`~mongoengine.queryset.QuerySet` class may be
|
||||
the way to go. To use a custom :class:`~mongoengine.queryset.QuerySet` class on
|
||||
a document, set ``queryset_class`` to the custom class in a
|
||||
:class:`~mongoengine.Document`\ s ``meta`` dictionary::
|
||||
|
||||
class AwesomerQuerySet(QuerySet):
|
||||
pass
|
||||
|
||||
class Page(Document):
|
||||
meta = {'queryset_class': AwesomerQuerySet}
|
||||
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Aggregation
|
||||
===========
|
||||
MongoDB provides some aggregation methods out of the box, but there are not as
|
||||
@@ -397,14 +448,17 @@ that you may use with these methods:
|
||||
* ``unset`` -- delete a particular value (since MongoDB v1.3+)
|
||||
* ``inc`` -- increment a value by a given amount
|
||||
* ``dec`` -- decrement a value by a given amount
|
||||
* ``pop`` -- remove the last item from a list
|
||||
* ``push`` -- append a value to a list
|
||||
* ``push_all`` -- append several values to a list
|
||||
* ``pop`` -- remove the first or last element of a list
|
||||
* ``pull`` -- remove a value from a list
|
||||
* ``pull_all`` -- remove several values from a list
|
||||
* ``add_to_set`` -- add value to a list only if its not in the list already
|
||||
|
||||
The syntax for atomic updates is similar to the querying syntax, but the
|
||||
modifier comes before the field, not after it::
|
||||
|
||||
|
||||
>>> post = BlogPost(title='Test', page_views=0, tags=['database'])
|
||||
>>> post.save()
|
||||
>>> BlogPost.objects(id=post.id).update_one(inc__page_views=1)
|
||||
|
@@ -7,7 +7,7 @@ MongoDB. To install it, simply run
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# easy_install -U mongoengine
|
||||
# pip install -U mongoengine
|
||||
|
||||
The source is available on `GitHub <http://github.com/hmarr/mongoengine>`_.
|
||||
|
||||
|
@@ -12,7 +12,7 @@ __all__ = (document.__all__ + fields.__all__ + connection.__all__ +
|
||||
|
||||
__author__ = 'Harry Marr'
|
||||
|
||||
VERSION = (0, 3, 0)
|
||||
VERSION = (0, 4, 1)
|
||||
|
||||
def get_version():
|
||||
version = '%s.%s' % (VERSION[0], VERSION[1])
|
||||
|
@@ -1,5 +1,8 @@
|
||||
from queryset import QuerySet, QuerySetManager
|
||||
from queryset import DoesNotExist, MultipleObjectsReturned
|
||||
|
||||
import sys
|
||||
import bson
|
||||
import pymongo
|
||||
|
||||
|
||||
@@ -18,11 +21,13 @@ class BaseField(object):
|
||||
may be added to subclasses of `Document` to define a document's schema.
|
||||
"""
|
||||
|
||||
# Fields may have _types inserted into indexes by default
|
||||
# Fields may have _types inserted into indexes by default
|
||||
_index_with_types = True
|
||||
|
||||
def __init__(self, db_field=None, name=None, required=False, default=None,
|
||||
unique=False, unique_with=None, primary_key=False):
|
||||
_geo_index = False
|
||||
|
||||
def __init__(self, db_field=None, name=None, required=False, default=None,
|
||||
unique=False, unique_with=None, primary_key=False,
|
||||
validation=None, choices=None):
|
||||
self.db_field = (db_field or name) if not primary_key else '_id'
|
||||
if name:
|
||||
import warnings
|
||||
@@ -34,9 +39,11 @@ class BaseField(object):
|
||||
self.unique = bool(unique or unique_with)
|
||||
self.unique_with = unique_with
|
||||
self.primary_key = primary_key
|
||||
self.validation = validation
|
||||
self.choices = choices
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""Descriptor for retrieving a value from a field in a document. Do
|
||||
"""Descriptor for retrieving a value from a field in a document. Do
|
||||
any necessary conversion between Python and MongoDB types.
|
||||
"""
|
||||
if instance is None:
|
||||
@@ -77,22 +84,39 @@ class BaseField(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _validate(self, value):
|
||||
# check choices
|
||||
if self.choices is not None:
|
||||
if value not in self.choices:
|
||||
raise ValidationError("Value must be one of %s."
|
||||
% unicode(self.choices))
|
||||
|
||||
# check validation argument
|
||||
if self.validation is not None:
|
||||
if callable(self.validation):
|
||||
if not self.validation(value):
|
||||
raise ValidationError('Value does not match custom' \
|
||||
'validation method.')
|
||||
else:
|
||||
raise ValueError('validation argument must be a callable.')
|
||||
|
||||
self.validate(value)
|
||||
|
||||
class ObjectIdField(BaseField):
|
||||
"""An field wrapper around MongoDB's ObjectIds.
|
||||
"""
|
||||
|
||||
|
||||
def to_python(self, value):
|
||||
return value
|
||||
# return unicode(value)
|
||||
|
||||
def to_mongo(self, value):
|
||||
if not isinstance(value, pymongo.objectid.ObjectId):
|
||||
if not isinstance(value, bson.objectid.ObjectId):
|
||||
try:
|
||||
return pymongo.objectid.ObjectId(str(value))
|
||||
return bson.objectid.ObjectId(unicode(value))
|
||||
except Exception, e:
|
||||
#e.message attribute has been deprecated since Python 2.6
|
||||
raise ValidationError(str(e))
|
||||
raise ValidationError(unicode(e))
|
||||
return value
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
@@ -100,7 +124,7 @@ class ObjectIdField(BaseField):
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
pymongo.objectid.ObjectId(str(value))
|
||||
bson.objectid.ObjectId(unicode(value))
|
||||
except:
|
||||
raise ValidationError('Invalid Object ID')
|
||||
|
||||
@@ -127,10 +151,10 @@ class DocumentMetaclass(type):
|
||||
# Get superclasses from superclass
|
||||
superclasses[base._class_name] = base
|
||||
superclasses.update(base._superclasses)
|
||||
|
||||
|
||||
if hasattr(base, '_meta'):
|
||||
# Ensure that the Document class may be subclassed -
|
||||
# inheritance may be disabled to remove dependency on
|
||||
# Ensure that the Document class may be subclassed -
|
||||
# inheritance may be disabled to remove dependency on
|
||||
# additional fields _cls and _types
|
||||
if base._meta.get('allow_inheritance', True) == False:
|
||||
raise ValueError('Document %s may not be subclassed' %
|
||||
@@ -167,8 +191,27 @@ class DocumentMetaclass(type):
|
||||
for field in new_class._fields.values():
|
||||
field.owner_document = new_class
|
||||
|
||||
module = attrs.get('__module__')
|
||||
|
||||
base_excs = tuple(base.DoesNotExist for base in bases
|
||||
if hasattr(base, 'DoesNotExist')) or (DoesNotExist,)
|
||||
exc = subclass_exception('DoesNotExist', base_excs, module)
|
||||
new_class.add_to_class('DoesNotExist', exc)
|
||||
|
||||
base_excs = tuple(base.MultipleObjectsReturned for base in bases
|
||||
if hasattr(base, 'MultipleObjectsReturned'))
|
||||
base_excs = base_excs or (MultipleObjectsReturned,)
|
||||
exc = subclass_exception('MultipleObjectsReturned', base_excs, module)
|
||||
new_class.add_to_class('MultipleObjectsReturned', exc)
|
||||
|
||||
global _document_registry
|
||||
_document_registry[name] = new_class
|
||||
|
||||
return new_class
|
||||
|
||||
def add_to_class(self, name, value):
|
||||
setattr(self, name, value)
|
||||
|
||||
|
||||
class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
"""Metaclass for top-level documents (i.e. documents that have their own
|
||||
@@ -176,27 +219,31 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
global _document_registry
|
||||
|
||||
super_new = super(TopLevelDocumentMetaclass, cls).__new__
|
||||
# Classes defined in this package are abstract and should not have
|
||||
# Classes defined in this package are abstract and should not have
|
||||
# their own metadata with DB collection, etc.
|
||||
# __metaclass__ is only set on the class with the __metaclass__
|
||||
# __metaclass__ is only set on the class with the __metaclass__
|
||||
# attribute (i.e. it is not set on subclasses). This differentiates
|
||||
# 'real' documents from the 'Document' class
|
||||
if attrs.get('__metaclass__') == TopLevelDocumentMetaclass:
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
collection = name.lower()
|
||||
|
||||
|
||||
id_field = None
|
||||
base_indexes = []
|
||||
base_meta = {}
|
||||
|
||||
# Subclassed documents inherit collection from superclass
|
||||
for base in bases:
|
||||
if hasattr(base, '_meta') and 'collection' in base._meta:
|
||||
collection = base._meta['collection']
|
||||
|
||||
# Propagate index options.
|
||||
for key in ('index_background', 'index_drop_dups', 'index_opts'):
|
||||
if key in base._meta:
|
||||
base_meta[key] = base._meta[key]
|
||||
|
||||
id_field = id_field or base._meta.get('id_field')
|
||||
base_indexes += base._meta.get('indexes', [])
|
||||
|
||||
@@ -207,7 +254,12 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
'ordering': [], # default ordering applied at runtime
|
||||
'indexes': [], # indexes to be ensured at runtime
|
||||
'id_field': id_field,
|
||||
'index_background': False,
|
||||
'index_drop_dups': False,
|
||||
'index_opts': {},
|
||||
'queryset_class': QuerySet,
|
||||
}
|
||||
meta.update(base_meta)
|
||||
|
||||
# Apply document-defined meta options
|
||||
meta.update(attrs.get('meta', {}))
|
||||
@@ -216,18 +268,21 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
# Set up collection manager, needs the class to have fields so use
|
||||
# DocumentMetaclass before instantiating CollectionManager object
|
||||
new_class = super_new(cls, name, bases, attrs)
|
||||
new_class.objects = QuerySetManager()
|
||||
|
||||
# Provide a default queryset unless one has been manually provided
|
||||
if not hasattr(new_class, 'objects'):
|
||||
new_class.objects = QuerySetManager()
|
||||
|
||||
user_indexes = [QuerySet._build_index_spec(new_class, spec)
|
||||
for spec in meta['indexes']] + base_indexes
|
||||
new_class._meta['indexes'] = user_indexes
|
||||
|
||||
|
||||
unique_indexes = []
|
||||
for field_name, field in new_class._fields.items():
|
||||
# Generate a list of indexes needed by uniqueness constraints
|
||||
if field.unique:
|
||||
field.required = True
|
||||
unique_fields = [field_name]
|
||||
unique_fields = [field.db_field]
|
||||
|
||||
# Add any unique_with fields to the back of the index spec
|
||||
if field.unique_with:
|
||||
@@ -252,13 +307,14 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
|
||||
# Check for custom primary key
|
||||
if field.primary_key:
|
||||
if not new_class._meta['id_field']:
|
||||
current_pk = new_class._meta['id_field']
|
||||
if current_pk and current_pk != field_name:
|
||||
raise ValueError('Cannot override primary key field')
|
||||
|
||||
if not current_pk:
|
||||
new_class._meta['id_field'] = field_name
|
||||
# Make 'Document.id' an alias to the real primary key field
|
||||
new_class.id = field
|
||||
#new_class._fields['id'] = field
|
||||
else:
|
||||
raise ValueError('Cannot override primary key field')
|
||||
|
||||
new_class._meta['unique_indexes'] = unique_indexes
|
||||
|
||||
@@ -267,8 +323,6 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
new_class._fields['id'] = ObjectIdField(db_field='_id')
|
||||
new_class.id = new_class._fields['id']
|
||||
|
||||
_document_registry[name] = new_class
|
||||
|
||||
return new_class
|
||||
|
||||
|
||||
@@ -276,31 +330,34 @@ class BaseDocument(object):
|
||||
|
||||
def __init__(self, **values):
|
||||
self._data = {}
|
||||
# Assign default values to instance
|
||||
for attr_name in self._fields.keys():
|
||||
# Use default value if present
|
||||
value = getattr(self, attr_name, None)
|
||||
setattr(self, attr_name, value)
|
||||
# Assign initial values to instance
|
||||
for attr_name, attr_value in self._fields.items():
|
||||
if attr_name in values:
|
||||
for attr_name in values.keys():
|
||||
try:
|
||||
setattr(self, attr_name, values.pop(attr_name))
|
||||
else:
|
||||
# Use default value if present
|
||||
value = getattr(self, attr_name, None)
|
||||
setattr(self, attr_name, value)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def validate(self):
|
||||
"""Ensure that all fields' values are valid and that required fields
|
||||
are present.
|
||||
"""
|
||||
# Get a list of tuples of field names and their current values
|
||||
fields = [(field, getattr(self, name))
|
||||
fields = [(field, getattr(self, name))
|
||||
for name, field in self._fields.items()]
|
||||
|
||||
# Ensure that each field is matched to a valid value
|
||||
for field, value in fields:
|
||||
if value is not None:
|
||||
try:
|
||||
field.validate(value)
|
||||
field._validate(value)
|
||||
except (ValueError, AttributeError, AssertionError), e:
|
||||
raise ValidationError('Invalid value for field of type "' +
|
||||
field.__class__.__name__ + '"')
|
||||
raise ValidationError('Invalid value for field of type "%s": %s'
|
||||
% (field.__class__.__name__, value))
|
||||
elif field.required:
|
||||
raise ValidationError('Field "%s" is required' % field.name)
|
||||
|
||||
@@ -319,6 +376,16 @@ class BaseDocument(object):
|
||||
all_subclasses.update(subclass._get_subclasses())
|
||||
return all_subclasses
|
||||
|
||||
@apply
|
||||
def pk():
|
||||
"""Primary key alias
|
||||
"""
|
||||
def fget(self):
|
||||
return getattr(self, self._meta['id_field'])
|
||||
def fset(self, value):
|
||||
return setattr(self, self._meta['id_field'], value)
|
||||
return property(fget, fset)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._fields)
|
||||
|
||||
@@ -375,8 +442,10 @@ class BaseDocument(object):
|
||||
self._meta.get('allow_inheritance', True) == False):
|
||||
data['_cls'] = self._class_name
|
||||
data['_types'] = self._superclasses.keys() + [self._class_name]
|
||||
if data.has_key('_id') and not data['_id']:
|
||||
del data['_id']
|
||||
return data
|
||||
|
||||
|
||||
@classmethod
|
||||
def _from_son(cls, son):
|
||||
"""Create an instance of a Document (subclass) from a PyMongo SON.
|
||||
@@ -406,14 +475,24 @@ class BaseDocument(object):
|
||||
|
||||
for field_name, field in cls._fields.items():
|
||||
if field.db_field in data:
|
||||
data[field_name] = field.to_python(data[field.db_field])
|
||||
value = data[field.db_field]
|
||||
data[field_name] = (value if value is None
|
||||
else field.to_python(value))
|
||||
|
||||
obj = cls(**data)
|
||||
obj._present_fields = present_fields
|
||||
return obj
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__) and hasattr(other, 'id'):
|
||||
if self.id == other.id:
|
||||
return True
|
||||
return False
|
||||
|
||||
if sys.version_info < (2, 5):
|
||||
# Prior to Python 2.5, Exception was an old-style class
|
||||
def subclass_exception(name, parents, unused):
|
||||
return types.ClassType(name, parents, {})
|
||||
else:
|
||||
def subclass_exception(name, parents, module):
|
||||
return type(name, parents, {'__module__': module})
|
||||
|
@@ -1,62 +1,71 @@
|
||||
from pymongo import Connection
|
||||
|
||||
import multiprocessing
|
||||
|
||||
__all__ = ['ConnectionError', 'connect']
|
||||
|
||||
|
||||
_connection_settings = {
|
||||
_connection_defaults = {
|
||||
'host': 'localhost',
|
||||
'port': 27017,
|
||||
}
|
||||
_connection = None
|
||||
_connection = {}
|
||||
_connection_settings = _connection_defaults.copy()
|
||||
|
||||
_db_name = None
|
||||
_db_username = None
|
||||
_db_password = None
|
||||
_db = None
|
||||
_db = {}
|
||||
|
||||
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _get_connection():
|
||||
def _get_connection(reconnect=False):
|
||||
global _connection
|
||||
identity = get_identity()
|
||||
# Connect to the database if not already connected
|
||||
if _connection is None:
|
||||
if _connection.get(identity) is None or reconnect:
|
||||
try:
|
||||
_connection = Connection(**_connection_settings)
|
||||
_connection[identity] = Connection(**_connection_settings)
|
||||
except:
|
||||
raise ConnectionError('Cannot connect to the database')
|
||||
return _connection
|
||||
return _connection[identity]
|
||||
|
||||
def _get_db():
|
||||
def _get_db(reconnect=False):
|
||||
global _db, _connection
|
||||
identity = get_identity()
|
||||
# Connect if not already connected
|
||||
if _connection is None:
|
||||
_connection = _get_connection()
|
||||
if _connection.get(identity) is None or reconnect:
|
||||
_connection[identity] = _get_connection(reconnect=reconnect)
|
||||
|
||||
if _db is None:
|
||||
if _db.get(identity) is None or reconnect:
|
||||
# _db_name will be None if the user hasn't called connect()
|
||||
if _db_name is None:
|
||||
raise ConnectionError('Not connected to the database')
|
||||
|
||||
# Get DB from current connection and authenticate if necessary
|
||||
_db = _connection[_db_name]
|
||||
_db[identity] = _connection[identity][_db_name]
|
||||
if _db_username and _db_password:
|
||||
_db.authenticate(_db_username, _db_password)
|
||||
_db[identity].authenticate(_db_username, _db_password)
|
||||
|
||||
return _db
|
||||
return _db[identity]
|
||||
|
||||
def get_identity():
|
||||
identity = multiprocessing.current_process()._identity
|
||||
identity = 0 if not identity else identity[0]
|
||||
return identity
|
||||
|
||||
def connect(db, username=None, password=None, **kwargs):
|
||||
"""Connect to the database specified by the 'db' argument. Connection
|
||||
settings may be provided here as well if the database is not running on
|
||||
the default port on localhost. If authentication is needed, provide
|
||||
username and password arguments as well.
|
||||
"""
|
||||
global _connection_settings, _db_name, _db_username, _db_password
|
||||
_connection_settings.update(kwargs)
|
||||
global _connection_settings, _db_name, _db_username, _db_password, _db
|
||||
_connection_settings = dict(_connection_defaults, **kwargs)
|
||||
_db_name = db
|
||||
_db_username = username
|
||||
_db_password = password
|
||||
return _get_db()
|
||||
return _get_db(reconnect=True)
|
||||
|
||||
|
@@ -32,6 +32,9 @@ class User(Document):
|
||||
last_login = DateTimeField(default=datetime.datetime.now)
|
||||
date_joined = DateTimeField(default=datetime.datetime.now)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.username
|
||||
|
||||
def get_full_name(self):
|
||||
"""Returns the users first and last names, separated by a space.
|
||||
"""
|
||||
@@ -72,10 +75,9 @@ class User(Document):
|
||||
email address.
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
|
||||
|
||||
# Normalize the address by lowercasing the domain part of the email
|
||||
# address.
|
||||
# Not sure why we'r allowing null email when its not allowed in django
|
||||
if email is not None:
|
||||
try:
|
||||
email_name, domain_part = email.strip().split('@', 1)
|
||||
@@ -83,12 +85,12 @@ class User(Document):
|
||||
pass
|
||||
else:
|
||||
email = '@'.join([email_name, domain_part.lower()])
|
||||
|
||||
|
||||
user = User(username=username, email=email, date_joined=now)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
def get_and_delete_messages(self):
|
||||
return []
|
||||
|
||||
|
45
mongoengine/django/shortcuts.py
Normal file
45
mongoengine/django/shortcuts.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.http import Http404
|
||||
from mongoengine.queryset import QuerySet
|
||||
from mongoengine.base import BaseDocument
|
||||
|
||||
def _get_queryset(cls):
|
||||
"""Inspired by django.shortcuts.*"""
|
||||
if isinstance(cls, QuerySet):
|
||||
return cls
|
||||
else:
|
||||
return cls.objects
|
||||
|
||||
def get_document_or_404(cls, *args, **kwargs):
|
||||
"""
|
||||
Uses get() to return an document, or raises a Http404 exception if the document
|
||||
does not exist.
|
||||
|
||||
cls may be a Document or QuerySet object. All other passed
|
||||
arguments and keyword arguments are used in the get() query.
|
||||
|
||||
Note: Like with get(), an MultipleObjectsReturned will be raised if more than one
|
||||
object is found.
|
||||
|
||||
Inspired by django.shortcuts.*
|
||||
"""
|
||||
queryset = _get_queryset(cls)
|
||||
try:
|
||||
return queryset.get(*args, **kwargs)
|
||||
except queryset._document.DoesNotExist:
|
||||
raise Http404('No %s matches the given query.' % queryset._document._class_name)
|
||||
|
||||
def get_list_or_404(cls, *args, **kwargs):
|
||||
"""
|
||||
Uses filter() to return a list of documents, or raise a Http404 exception if
|
||||
the list is empty.
|
||||
|
||||
cls may be a Document or QuerySet object. All other passed
|
||||
arguments and keyword arguments are used in the filter() query.
|
||||
|
||||
Inspired by django.shortcuts.*
|
||||
"""
|
||||
queryset = _get_queryset(cls)
|
||||
obj_list = list(queryset.filter(*args, **kwargs))
|
||||
if not obj_list:
|
||||
raise Http404('No %s matches the given query.' % queryset._document._class_name)
|
||||
return obj_list
|
112
mongoengine/django/storage.py
Normal file
112
mongoengine/django/storage.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import os
|
||||
import itertools
|
||||
import urlparse
|
||||
|
||||
from mongoengine import *
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import Storage
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
class FileDocument(Document):
|
||||
"""A document used to store a single file in GridFS.
|
||||
"""
|
||||
file = FileField()
|
||||
|
||||
|
||||
class GridFSStorage(Storage):
|
||||
"""A custom storage backend to store files in GridFS
|
||||
"""
|
||||
|
||||
def __init__(self, base_url=None):
|
||||
|
||||
if base_url is None:
|
||||
base_url = settings.MEDIA_URL
|
||||
self.base_url = base_url
|
||||
self.document = FileDocument
|
||||
self.field = 'file'
|
||||
|
||||
def delete(self, name):
|
||||
"""Deletes the specified file from the storage system.
|
||||
"""
|
||||
if self.exists(name):
|
||||
doc = self.document.objects.first()
|
||||
field = getattr(doc, self.field)
|
||||
self._get_doc_with_name(name).delete() # Delete the FileField
|
||||
field.delete() # Delete the FileDocument
|
||||
|
||||
def exists(self, name):
|
||||
"""Returns True if a file referened by the given name already exists in the
|
||||
storage system, or False if the name is available for a new file.
|
||||
"""
|
||||
doc = self._get_doc_with_name(name)
|
||||
if doc:
|
||||
field = getattr(doc, self.field)
|
||||
return bool(field.name)
|
||||
else:
|
||||
return False
|
||||
|
||||
def listdir(self, path=None):
|
||||
"""Lists the contents of the specified path, returning a 2-tuple of lists;
|
||||
the first item being directories, the second item being files.
|
||||
"""
|
||||
def name(doc):
|
||||
return getattr(doc, self.field).name
|
||||
docs = self.document.objects
|
||||
return [], [name(d) for d in docs if name(d)]
|
||||
|
||||
def size(self, name):
|
||||
"""Returns the total size, in bytes, of the file specified by name.
|
||||
"""
|
||||
doc = self._get_doc_with_name(name)
|
||||
if doc:
|
||||
return getattr(doc, self.field).length
|
||||
else:
|
||||
raise ValueError("No such file or directory: '%s'" % name)
|
||||
|
||||
def url(self, name):
|
||||
"""Returns an absolute URL where the file's contents can be accessed
|
||||
directly by a web browser.
|
||||
"""
|
||||
if self.base_url is None:
|
||||
raise ValueError("This file is not accessible via a URL.")
|
||||
return urlparse.urljoin(self.base_url, name).replace('\\', '/')
|
||||
|
||||
def _get_doc_with_name(self, name):
|
||||
"""Find the documents in the store with the given name
|
||||
"""
|
||||
docs = self.document.objects
|
||||
doc = [d for d in docs if getattr(d, self.field).name == name]
|
||||
if doc:
|
||||
return doc[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _open(self, name, mode='rb'):
|
||||
doc = self._get_doc_with_name(name)
|
||||
if doc:
|
||||
return getattr(doc, self.field)
|
||||
else:
|
||||
raise ValueError("No file found with the name '%s'." % name)
|
||||
|
||||
def get_available_name(self, name):
|
||||
"""Returns a filename that's free on the target storage system, and
|
||||
available for new content to be written to.
|
||||
"""
|
||||
file_root, file_ext = os.path.splitext(name)
|
||||
# If the filename already exists, add an underscore and a number (before
|
||||
# the file extension, if one exists) to the filename until the generated
|
||||
# filename doesn't exist.
|
||||
count = itertools.count(1)
|
||||
while self.exists(name):
|
||||
# file_ext includes the dot.
|
||||
name = os.path.join("%s_%s%s" % (file_root, count.next(), file_ext))
|
||||
|
||||
return name
|
||||
|
||||
def _save(self, name, content):
|
||||
doc = self.document()
|
||||
getattr(doc, self.field).put(content, filename=name)
|
||||
doc.save()
|
||||
|
||||
return name
|
21
mongoengine/django/tests.py
Normal file
21
mongoengine/django/tests.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#coding: utf-8
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
|
||||
from mongoengine import connect
|
||||
|
||||
class MongoTestCase(TestCase):
|
||||
"""
|
||||
TestCase class that clear the collection between the tests
|
||||
"""
|
||||
db_name = 'test_%s' % settings.MONGO_DATABASE_NAME
|
||||
def __init__(self, methodName='runtest'):
|
||||
self.db = connect(self.db_name)
|
||||
super(MongoTestCase, self).__init__(methodName)
|
||||
|
||||
def _post_teardown(self):
|
||||
super(MongoTestCase, self)._post_teardown()
|
||||
for collection in self.db.collection_names():
|
||||
if collection == 'system.indexes':
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
@@ -15,7 +15,7 @@ class EmbeddedDocument(BaseDocument):
|
||||
fields on :class:`~mongoengine.Document`\ s through the
|
||||
:class:`~mongoengine.EmbeddedDocumentField` field type.
|
||||
"""
|
||||
|
||||
|
||||
__metaclass__ = DocumentMetaclass
|
||||
|
||||
|
||||
@@ -40,35 +40,37 @@ class Document(BaseDocument):
|
||||
presence of `_cls` and `_types`, set :attr:`allow_inheritance` to
|
||||
``False`` in the :attr:`meta` dictionary.
|
||||
|
||||
A :class:`~mongoengine.Document` may use a **Capped Collection** by
|
||||
A :class:`~mongoengine.Document` may use a **Capped Collection** by
|
||||
specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta`
|
||||
dictionary. :attr:`max_documents` is the maximum number of documents that
|
||||
is allowed to be stored in the collection, and :attr:`max_size` is the
|
||||
maximum size of the collection in bytes. If :attr:`max_size` is not
|
||||
specified and :attr:`max_documents` is, :attr:`max_size` defaults to
|
||||
is allowed to be stored in the collection, and :attr:`max_size` is the
|
||||
maximum size of the collection in bytes. If :attr:`max_size` is not
|
||||
specified and :attr:`max_documents` is, :attr:`max_size` defaults to
|
||||
10000000 bytes (10MB).
|
||||
|
||||
Indexes may be created by specifying :attr:`indexes` in the :attr:`meta`
|
||||
dictionary. The value should be a list of field names or tuples of field
|
||||
dictionary. The value should be a list of field names or tuples of field
|
||||
names. Index direction may be specified by prefixing the field names with
|
||||
a **+** or **-** sign.
|
||||
"""
|
||||
|
||||
__metaclass__ = TopLevelDocumentMetaclass
|
||||
|
||||
def save(self, safe=True, force_insert=False):
|
||||
def save(self, safe=True, force_insert=False, validate=True):
|
||||
"""Save the :class:`~mongoengine.Document` to the database. If the
|
||||
document already exists, it will be updated, otherwise it will be
|
||||
created.
|
||||
|
||||
If ``safe=True`` and the operation is unsuccessful, an
|
||||
If ``safe=True`` and the operation is unsuccessful, an
|
||||
:class:`~mongoengine.OperationError` will be raised.
|
||||
|
||||
:param safe: check if the operation succeeded before returning
|
||||
:param force_insert: only try to create a new document, don't allow
|
||||
:param force_insert: only try to create a new document, don't allow
|
||||
updates of existing documents
|
||||
:param validate: validates the document; set to ``False`` for skiping
|
||||
"""
|
||||
self.validate()
|
||||
if validate:
|
||||
self.validate()
|
||||
doc = self.to_mongo()
|
||||
try:
|
||||
collection = self.__class__.objects._collection
|
||||
@@ -119,31 +121,31 @@ class Document(BaseDocument):
|
||||
|
||||
class MapReduceDocument(object):
|
||||
"""A document returned from a map/reduce query.
|
||||
|
||||
|
||||
:param collection: An instance of :class:`~pymongo.Collection`
|
||||
:param key: Document/result key, often an instance of
|
||||
:class:`~pymongo.objectid.ObjectId`. If supplied as
|
||||
an ``ObjectId`` found in the given ``collection``,
|
||||
:param key: Document/result key, often an instance of
|
||||
:class:`~bson.objectid.ObjectId`. If supplied as
|
||||
an ``ObjectId`` found in the given ``collection``,
|
||||
the object can be accessed via the ``object`` property.
|
||||
:param value: The result(s) for this key.
|
||||
|
||||
|
||||
.. versionadded:: 0.3
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, document, collection, key, value):
|
||||
self._document = document
|
||||
self._collection = collection
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
|
||||
@property
|
||||
def object(self):
|
||||
"""Lazy-load the object referenced by ``self.key``. ``self.key``
|
||||
"""Lazy-load the object referenced by ``self.key``. ``self.key``
|
||||
should be the ``primary_key``.
|
||||
"""
|
||||
id_field = self._document()._meta['id_field']
|
||||
id_field_type = type(id_field)
|
||||
|
||||
|
||||
if not isinstance(self.key, id_field_type):
|
||||
try:
|
||||
self.key = id_field_type(self.key)
|
||||
|
@@ -1,18 +1,25 @@
|
||||
from base import BaseField, ObjectIdField, ValidationError, get_document
|
||||
from document import Document, EmbeddedDocument
|
||||
from connection import _get_db
|
||||
from operator import itemgetter
|
||||
|
||||
import re
|
||||
import pymongo
|
||||
import bson.dbref
|
||||
import bson.son
|
||||
import bson.binary
|
||||
import datetime
|
||||
import decimal
|
||||
import gridfs
|
||||
import warnings
|
||||
import types
|
||||
|
||||
|
||||
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
|
||||
'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField',
|
||||
'ObjectIdField', 'ReferenceField', 'ValidationError',
|
||||
'DecimalField', 'URLField', 'GenericReferenceField',
|
||||
'BinaryField']
|
||||
'DecimalField', 'URLField', 'GenericReferenceField', 'FileField',
|
||||
'BinaryField', 'SortedListField', 'EmailField', 'GeoPointField']
|
||||
|
||||
RECURSIVE_REFERENCE_CONSTANT = 'self'
|
||||
|
||||
@@ -21,9 +28,10 @@ class StringField(BaseField):
|
||||
"""A unicode string field.
|
||||
"""
|
||||
|
||||
def __init__(self, regex=None, max_length=None, **kwargs):
|
||||
def __init__(self, regex=None, max_length=None, min_length=None, **kwargs):
|
||||
self.regex = re.compile(regex) if regex else None
|
||||
self.max_length = max_length
|
||||
self.min_length = min_length
|
||||
super(StringField, self).__init__(**kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
@@ -35,6 +43,9 @@ class StringField(BaseField):
|
||||
if self.max_length is not None and len(value) > self.max_length:
|
||||
raise ValidationError('String value is too long')
|
||||
|
||||
if self.min_length is not None and len(value) < self.min_length:
|
||||
raise ValidationError('String value is too short')
|
||||
|
||||
if self.regex is not None and self.regex.match(value) is None:
|
||||
message = 'String value did not match validation regex'
|
||||
raise ValidationError(message)
|
||||
@@ -46,7 +57,7 @@ class StringField(BaseField):
|
||||
if not isinstance(op, basestring):
|
||||
return value
|
||||
|
||||
if op.lstrip('i') in ('startswith', 'endswith', 'contains'):
|
||||
if op.lstrip('i') in ('startswith', 'endswith', 'contains', 'exact'):
|
||||
flags = 0
|
||||
if op.startswith('i'):
|
||||
flags = re.IGNORECASE
|
||||
@@ -57,12 +68,17 @@ class StringField(BaseField):
|
||||
regex = r'^%s'
|
||||
elif op == 'endswith':
|
||||
regex = r'%s$'
|
||||
elif op == 'exact':
|
||||
regex = r'^%s$'
|
||||
|
||||
# escape unsafe characters which could lead to a re.error
|
||||
value = re.escape(value)
|
||||
value = re.compile(regex % value, flags)
|
||||
return value
|
||||
|
||||
|
||||
class URLField(StringField):
|
||||
"""A field that validates input as a URL.
|
||||
"""A field that validates input as an URL.
|
||||
|
||||
.. versionadded:: 0.3
|
||||
"""
|
||||
@@ -94,6 +110,23 @@ class URLField(StringField):
|
||||
raise ValidationError(message)
|
||||
|
||||
|
||||
class EmailField(StringField):
|
||||
"""A field that validates input as an E-Mail-Address.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
"""
|
||||
|
||||
EMAIL_REGEX = re.compile(
|
||||
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
|
||||
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
|
||||
r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE # domain
|
||||
)
|
||||
|
||||
def validate(self, value):
|
||||
if not EmailField.EMAIL_REGEX.match(value):
|
||||
raise ValidationError('Invalid Mail-address: %s' % value)
|
||||
|
||||
|
||||
class IntField(BaseField):
|
||||
"""An integer field.
|
||||
"""
|
||||
@@ -140,7 +173,6 @@ class FloatField(BaseField):
|
||||
if self.max_value is not None and value > self.max_value:
|
||||
raise ValidationError('Float value is too large')
|
||||
|
||||
|
||||
class DecimalField(BaseField):
|
||||
"""A fixed-point decimal number field.
|
||||
|
||||
@@ -156,6 +188,9 @@ class DecimalField(BaseField):
|
||||
value = unicode(value)
|
||||
return decimal.Decimal(value)
|
||||
|
||||
def to_mongo(self, value):
|
||||
return unicode(value)
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, decimal.Decimal):
|
||||
if not isinstance(value, basestring):
|
||||
@@ -198,33 +233,43 @@ class EmbeddedDocumentField(BaseField):
|
||||
:class:`~mongoengine.EmbeddedDocument`.
|
||||
"""
|
||||
|
||||
def __init__(self, document, **kwargs):
|
||||
if not issubclass(document, EmbeddedDocument):
|
||||
raise ValidationError('Invalid embedded document class provided '
|
||||
'to an EmbeddedDocumentField')
|
||||
self.document = document
|
||||
def __init__(self, document_type, **kwargs):
|
||||
if not isinstance(document_type, basestring):
|
||||
if not issubclass(document_type, EmbeddedDocument):
|
||||
raise ValidationError('Invalid embedded document class '
|
||||
'provided to an EmbeddedDocumentField')
|
||||
self.document_type_obj = document_type
|
||||
super(EmbeddedDocumentField, self).__init__(**kwargs)
|
||||
|
||||
@property
|
||||
def document_type(self):
|
||||
if isinstance(self.document_type_obj, basestring):
|
||||
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
|
||||
self.document_type_obj = self.owner_document
|
||||
else:
|
||||
self.document_type_obj = get_document(self.document_type_obj)
|
||||
return self.document_type_obj
|
||||
|
||||
def to_python(self, value):
|
||||
if not isinstance(value, self.document):
|
||||
return self.document._from_son(value)
|
||||
if not isinstance(value, self.document_type):
|
||||
return self.document_type._from_son(value)
|
||||
return value
|
||||
|
||||
def to_mongo(self, value):
|
||||
return self.document.to_mongo(value)
|
||||
return self.document_type.to_mongo(value)
|
||||
|
||||
def validate(self, value):
|
||||
"""Make sure that the document instance is an instance of the
|
||||
EmbeddedDocument subclass provided when the document was defined.
|
||||
"""
|
||||
# Using isinstance also works for subclasses of self.document
|
||||
if not isinstance(value, self.document):
|
||||
if not isinstance(value, self.document_type):
|
||||
raise ValidationError('Invalid embedded document instance '
|
||||
'provided to an EmbeddedDocumentField')
|
||||
self.document.validate(value)
|
||||
self.document_type.validate(value)
|
||||
|
||||
def lookup_member(self, member_name):
|
||||
return self.document._fields.get(member_name)
|
||||
return self.document_type._fields.get(member_name)
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
return self.to_mongo(value)
|
||||
@@ -243,6 +288,7 @@ class ListField(BaseField):
|
||||
raise ValidationError('Argument to ListField constructor must be '
|
||||
'a valid field')
|
||||
self.field = field
|
||||
kwargs.setdefault('default', lambda: [])
|
||||
super(ListField, self).__init__(**kwargs)
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
@@ -254,13 +300,13 @@ class ListField(BaseField):
|
||||
|
||||
if isinstance(self.field, ReferenceField):
|
||||
referenced_type = self.field.document_type
|
||||
# Get value from document instance if available
|
||||
# Get value from document instance if available
|
||||
value_list = instance._data.get(self.name)
|
||||
if value_list:
|
||||
deref_list = []
|
||||
for value in value_list:
|
||||
# Dereference DBRefs
|
||||
if isinstance(value, (pymongo.dbref.DBRef)):
|
||||
if isinstance(value, (bson.dbref.DBRef)):
|
||||
value = _get_db().dereference(value)
|
||||
deref_list.append(referenced_type._from_son(value))
|
||||
else:
|
||||
@@ -273,7 +319,7 @@ class ListField(BaseField):
|
||||
deref_list = []
|
||||
for value in value_list:
|
||||
# Dereference DBRefs
|
||||
if isinstance(value, (dict, pymongo.son.SON)):
|
||||
if isinstance(value, (dict, bson.son.SON)):
|
||||
deref_list.append(self.field.dereference(value))
|
||||
else:
|
||||
deref_list.append(value)
|
||||
@@ -297,16 +343,47 @@ class ListField(BaseField):
|
||||
try:
|
||||
[self.field.validate(item) for item in value]
|
||||
except Exception, err:
|
||||
raise ValidationError('Invalid ListField item (%s)' % str(err))
|
||||
raise ValidationError('Invalid ListField item (%s)' % str(item))
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
if op in ('set', 'unset'):
|
||||
return [self.field.to_mongo(v) for v in value]
|
||||
return self.field.to_mongo(value)
|
||||
return [self.field.prepare_query_value(op, v) for v in value]
|
||||
return self.field.prepare_query_value(op, value)
|
||||
|
||||
def lookup_member(self, member_name):
|
||||
return self.field.lookup_member(member_name)
|
||||
|
||||
def _set_owner_document(self, owner_document):
|
||||
self.field.owner_document = owner_document
|
||||
self._owner_document = owner_document
|
||||
|
||||
def _get_owner_document(self, owner_document):
|
||||
self._owner_document = owner_document
|
||||
|
||||
owner_document = property(_get_owner_document, _set_owner_document)
|
||||
|
||||
|
||||
class SortedListField(ListField):
|
||||
"""A ListField that sorts the contents of its list before writing to
|
||||
the database in order to ensure that a sorted list is always
|
||||
retrieved.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
"""
|
||||
|
||||
_ordering = None
|
||||
|
||||
def __init__(self, field, **kwargs):
|
||||
if 'ordering' in kwargs.keys():
|
||||
self._ordering = kwargs.pop('ordering')
|
||||
super(SortedListField, self).__init__(field, **kwargs)
|
||||
|
||||
def to_mongo(self, value):
|
||||
if self._ordering is not None:
|
||||
return sorted([self.field.to_mongo(item) for item in value],
|
||||
key=itemgetter(self._ordering))
|
||||
return sorted([self.field.to_mongo(item) for item in value])
|
||||
|
||||
|
||||
class DictField(BaseField):
|
||||
"""A dictionary field that wraps a standard Python dictionary. This is
|
||||
@@ -315,6 +392,12 @@ class DictField(BaseField):
|
||||
.. versionadded:: 0.3
|
||||
"""
|
||||
|
||||
def __init__(self, basecls=None, *args, **kwargs):
|
||||
self.basecls = basecls or BaseField
|
||||
assert issubclass(self.basecls, BaseField)
|
||||
kwargs.setdefault('default', lambda: {})
|
||||
super(DictField, self).__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
"""Make sure that a list of valid fields is being used.
|
||||
"""
|
||||
@@ -327,8 +410,7 @@ class DictField(BaseField):
|
||||
'contain "." or "$" characters')
|
||||
|
||||
def lookup_member(self, member_name):
|
||||
return BaseField(db_field=member_name)
|
||||
|
||||
return self.basecls(db_field=member_name)
|
||||
|
||||
class ReferenceField(BaseField):
|
||||
"""A reference to a document that will be automatically dereferenced on
|
||||
@@ -341,7 +423,6 @@ class ReferenceField(BaseField):
|
||||
raise ValidationError('Argument to ReferenceField constructor '
|
||||
'must be a document class or a string')
|
||||
self.document_type_obj = document_type
|
||||
self.document_obj = None
|
||||
super(ReferenceField, self).__init__(**kwargs)
|
||||
|
||||
@property
|
||||
@@ -363,7 +444,7 @@ class ReferenceField(BaseField):
|
||||
# Get value from document instance if available
|
||||
value = instance._data.get(self.name)
|
||||
# Dereference DBRefs
|
||||
if isinstance(value, (pymongo.dbref.DBRef)):
|
||||
if isinstance(value, (bson.dbref.DBRef)):
|
||||
value = _get_db().dereference(value)
|
||||
if value is not None:
|
||||
instance._data[self.name] = self.document_type._from_son(value)
|
||||
@@ -385,13 +466,13 @@ class ReferenceField(BaseField):
|
||||
|
||||
id_ = id_field.to_mongo(id_)
|
||||
collection = self.document_type._meta['collection']
|
||||
return pymongo.dbref.DBRef(collection, id_)
|
||||
return bson.dbref.DBRef(collection, id_)
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
return self.to_mongo(value)
|
||||
|
||||
def validate(self, value):
|
||||
assert isinstance(value, (self.document_type, pymongo.dbref.DBRef))
|
||||
assert isinstance(value, (self.document_type, bson.dbref.DBRef))
|
||||
|
||||
def lookup_member(self, member_name):
|
||||
return self.document_type._fields.get(member_name)
|
||||
@@ -409,7 +490,7 @@ class GenericReferenceField(BaseField):
|
||||
return self
|
||||
|
||||
value = instance._data.get(self.name)
|
||||
if isinstance(value, (dict, pymongo.son.SON)):
|
||||
if isinstance(value, (dict, bson.son.SON)):
|
||||
instance._data[self.name] = self.dereference(value)
|
||||
|
||||
return super(GenericReferenceField, self).__get__(instance, owner)
|
||||
@@ -437,11 +518,12 @@ class GenericReferenceField(BaseField):
|
||||
|
||||
id_ = id_field.to_mongo(id_)
|
||||
collection = document._meta['collection']
|
||||
ref = pymongo.dbref.DBRef(collection, id_)
|
||||
ref = bson.dbref.DBRef(collection, id_)
|
||||
return {'_cls': document.__class__.__name__, '_ref': ref}
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
return self.to_mongo(value)['_ref']
|
||||
return self.to_mongo(value)
|
||||
|
||||
|
||||
class BinaryField(BaseField):
|
||||
"""A binary data field.
|
||||
@@ -452,9 +534,10 @@ class BinaryField(BaseField):
|
||||
super(BinaryField, self).__init__(**kwargs)
|
||||
|
||||
def to_mongo(self, value):
|
||||
return pymongo.binary.Binary(value)
|
||||
return bson.binary.Binary(value)
|
||||
|
||||
def to_python(self, value):
|
||||
# Returns str not unicode as this is binary data
|
||||
return str(value)
|
||||
|
||||
def validate(self, value):
|
||||
@@ -462,3 +545,161 @@ class BinaryField(BaseField):
|
||||
|
||||
if self.max_bytes is not None and len(value) > self.max_bytes:
|
||||
raise ValidationError('Binary value is too long')
|
||||
|
||||
|
||||
class GridFSError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GridFSProxy(object):
|
||||
"""Proxy object to handle writing and reading of files to and from GridFS
|
||||
|
||||
.. versionadded:: 0.4
|
||||
"""
|
||||
|
||||
def __init__(self, grid_id=None):
|
||||
self.fs = gridfs.GridFS(_get_db()) # Filesystem instance
|
||||
self.newfile = None # Used for partial writes
|
||||
self.grid_id = grid_id # Store GridFS id for file
|
||||
|
||||
def __getattr__(self, name):
|
||||
obj = self.get()
|
||||
if name in dir(obj):
|
||||
return getattr(obj, name)
|
||||
raise AttributeError
|
||||
|
||||
def __get__(self, instance, value):
|
||||
return self
|
||||
|
||||
def get(self, id=None):
|
||||
if id:
|
||||
self.grid_id = id
|
||||
try:
|
||||
return self.fs.get(id or self.grid_id)
|
||||
except:
|
||||
# File has been deleted
|
||||
return None
|
||||
|
||||
def new_file(self, **kwargs):
|
||||
self.newfile = self.fs.new_file(**kwargs)
|
||||
self.grid_id = self.newfile._id
|
||||
|
||||
def put(self, file, **kwargs):
|
||||
if self.grid_id:
|
||||
raise GridFSError('This document already has a file. Either delete '
|
||||
'it or call replace to overwrite it')
|
||||
self.grid_id = self.fs.put(file, **kwargs)
|
||||
|
||||
def write(self, string):
|
||||
if self.grid_id:
|
||||
if not self.newfile:
|
||||
raise GridFSError('This document already has a file. Either '
|
||||
'delete it or call replace to overwrite it')
|
||||
else:
|
||||
self.new_file()
|
||||
self.newfile.write(string)
|
||||
|
||||
def writelines(self, lines):
|
||||
if not self.newfile:
|
||||
self.new_file()
|
||||
self.grid_id = self.newfile._id
|
||||
self.newfile.writelines(lines)
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
return self.get().read()
|
||||
except:
|
||||
return None
|
||||
|
||||
def delete(self):
|
||||
# Delete file from GridFS, FileField still remains
|
||||
self.fs.delete(self.grid_id)
|
||||
self.grid_id = None
|
||||
|
||||
def replace(self, file, **kwargs):
|
||||
self.delete()
|
||||
self.put(file, **kwargs)
|
||||
|
||||
def close(self):
|
||||
if self.newfile:
|
||||
self.newfile.close()
|
||||
else:
|
||||
msg = "The close() method is only necessary after calling write()"
|
||||
warnings.warn(msg)
|
||||
|
||||
|
||||
class FileField(BaseField):
|
||||
"""A GridFS storage field.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FileField, self).__init__(**kwargs)
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
# Check if a file already exists for this model
|
||||
grid_file = instance._data.get(self.name)
|
||||
self.grid_file = grid_file
|
||||
if self.grid_file:
|
||||
return self.grid_file
|
||||
return GridFSProxy()
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if isinstance(value, file) or isinstance(value, str):
|
||||
# using "FileField() = file/string" notation
|
||||
grid_file = instance._data.get(self.name)
|
||||
# If a file already exists, delete it
|
||||
if grid_file:
|
||||
try:
|
||||
grid_file.delete()
|
||||
except:
|
||||
pass
|
||||
# Create a new file with the new data
|
||||
grid_file.put(value)
|
||||
else:
|
||||
# Create a new proxy object as we don't already have one
|
||||
instance._data[self.name] = GridFSProxy()
|
||||
instance._data[self.name].put(value)
|
||||
else:
|
||||
instance._data[self.name] = value
|
||||
|
||||
def to_mongo(self, value):
|
||||
# Store the GridFS file id in MongoDB
|
||||
if isinstance(value, GridFSProxy) and value.grid_id is not None:
|
||||
return value.grid_id
|
||||
return None
|
||||
|
||||
def to_python(self, value):
|
||||
if value is not None:
|
||||
return GridFSProxy(value)
|
||||
|
||||
def validate(self, value):
|
||||
if value.grid_id is not None:
|
||||
assert isinstance(value, GridFSProxy)
|
||||
assert isinstance(value.grid_id, bson.objectid.ObjectId)
|
||||
|
||||
|
||||
class GeoPointField(BaseField):
|
||||
"""A list storing a latitude and longitude.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
"""
|
||||
|
||||
_geo_index = True
|
||||
|
||||
def validate(self, value):
|
||||
"""Make sure that a geo-value is of type (x, y)
|
||||
"""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValidationError('GeoPointField can only accept tuples or '
|
||||
'lists of (x, y)')
|
||||
|
||||
if not len(value) == 2:
|
||||
raise ValidationError('Value must be a two-dimensional point.')
|
||||
if (not isinstance(value[0], (float, int)) and
|
||||
not isinstance(value[1], (float, int))):
|
||||
raise ValidationError('Both values in point must be float or int.')
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
import bson
|
||||
import pymongo
|
||||
|
||||
from mongoengine import *
|
||||
@@ -7,7 +8,7 @@ from mongoengine.connection import _get_db
|
||||
|
||||
|
||||
class DocumentTest(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = _get_db()
|
||||
@@ -38,7 +39,7 @@ class DocumentTest(unittest.TestCase):
|
||||
name = name_field
|
||||
age = age_field
|
||||
non_field = True
|
||||
|
||||
|
||||
self.assertEqual(Person._fields['name'], name_field)
|
||||
self.assertEqual(Person._fields['age'], age_field)
|
||||
self.assertFalse('non_field' in Person._fields)
|
||||
@@ -60,7 +61,7 @@ class DocumentTest(unittest.TestCase):
|
||||
|
||||
mammal_superclasses = {'Animal': Animal}
|
||||
self.assertEqual(Mammal._superclasses, mammal_superclasses)
|
||||
|
||||
|
||||
dog_superclasses = {
|
||||
'Animal': Animal,
|
||||
'Animal.Mammal': Mammal,
|
||||
@@ -68,7 +69,7 @@ class DocumentTest(unittest.TestCase):
|
||||
self.assertEqual(Dog._superclasses, dog_superclasses)
|
||||
|
||||
def test_get_subclasses(self):
|
||||
"""Ensure that the correct list of subclasses is retrieved by the
|
||||
"""Ensure that the correct list of subclasses is retrieved by the
|
||||
_get_subclasses method.
|
||||
"""
|
||||
class Animal(Document): pass
|
||||
@@ -78,15 +79,15 @@ class DocumentTest(unittest.TestCase):
|
||||
class Dog(Mammal): pass
|
||||
|
||||
mammal_subclasses = {
|
||||
'Animal.Mammal.Dog': Dog,
|
||||
'Animal.Mammal.Dog': Dog,
|
||||
'Animal.Mammal.Human': Human
|
||||
}
|
||||
self.assertEqual(Mammal._get_subclasses(), mammal_subclasses)
|
||||
|
||||
|
||||
animal_subclasses = {
|
||||
'Animal.Fish': Fish,
|
||||
'Animal.Mammal': Mammal,
|
||||
'Animal.Mammal.Dog': Dog,
|
||||
'Animal.Mammal.Dog': Dog,
|
||||
'Animal.Mammal.Human': Human
|
||||
}
|
||||
self.assertEqual(Animal._get_subclasses(), animal_subclasses)
|
||||
@@ -124,9 +125,14 @@ class DocumentTest(unittest.TestCase):
|
||||
|
||||
self.assertTrue('name' in Employee._fields)
|
||||
self.assertTrue('salary' in Employee._fields)
|
||||
self.assertEqual(Employee._meta['collection'],
|
||||
self.assertEqual(Employee._meta['collection'],
|
||||
self.Person._meta['collection'])
|
||||
|
||||
# Ensure that MRO error is not raised
|
||||
class A(Document): pass
|
||||
class B(A): pass
|
||||
class C(B): pass
|
||||
|
||||
def test_allow_inheritance(self):
|
||||
"""Ensure that inheritance may be disabled on simple classes and that
|
||||
_cls and _types will not be used.
|
||||
@@ -141,7 +147,7 @@ class DocumentTest(unittest.TestCase):
|
||||
class Dog(Animal):
|
||||
pass
|
||||
self.assertRaises(ValueError, create_dog_class)
|
||||
|
||||
|
||||
# Check that _cls etc aren't present on simple documents
|
||||
dog = Animal(name='dog')
|
||||
dog.save()
|
||||
@@ -156,7 +162,7 @@ class DocumentTest(unittest.TestCase):
|
||||
class Employee(self.Person):
|
||||
meta = {'allow_inheritance': False}
|
||||
self.assertRaises(ValueError, create_employee_class)
|
||||
|
||||
|
||||
# Test the same for embedded documents
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField()
|
||||
@@ -181,7 +187,7 @@ class DocumentTest(unittest.TestCase):
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
meta = {'collection': collection}
|
||||
|
||||
|
||||
user = Person(name="Test User")
|
||||
user.save()
|
||||
self.assertTrue(collection in self.db.collection_names())
|
||||
@@ -195,6 +201,37 @@ class DocumentTest(unittest.TestCase):
|
||||
Person.drop_collection()
|
||||
self.assertFalse(collection in self.db.collection_names())
|
||||
|
||||
def test_inherited_collections(self):
|
||||
"""Ensure that subclassed documents don't override parents' collections.
|
||||
"""
|
||||
class Drink(Document):
|
||||
name = StringField()
|
||||
|
||||
class AlcoholicDrink(Drink):
|
||||
meta = {'collection': 'booze'}
|
||||
|
||||
class Drinker(Document):
|
||||
drink = GenericReferenceField()
|
||||
|
||||
Drink.drop_collection()
|
||||
AlcoholicDrink.drop_collection()
|
||||
Drinker.drop_collection()
|
||||
|
||||
red_bull = Drink(name='Red Bull')
|
||||
red_bull.save()
|
||||
|
||||
programmer = Drinker(drink=red_bull)
|
||||
programmer.save()
|
||||
|
||||
beer = AlcoholicDrink(name='Beer')
|
||||
beer.save()
|
||||
|
||||
real_person = Drinker(drink=beer)
|
||||
real_person.save()
|
||||
|
||||
self.assertEqual(Drinker.objects[0].drink.name, red_bull.name)
|
||||
self.assertEqual(Drinker.objects[1].drink.name, beer.name)
|
||||
|
||||
def test_capped_collection(self):
|
||||
"""Ensure that capped collections work properly.
|
||||
"""
|
||||
@@ -244,7 +281,7 @@ class DocumentTest(unittest.TestCase):
|
||||
tags = ListField(StringField())
|
||||
meta = {
|
||||
'indexes': [
|
||||
'-date',
|
||||
'-date',
|
||||
'tags',
|
||||
('category', '-date')
|
||||
],
|
||||
@@ -259,12 +296,13 @@ class DocumentTest(unittest.TestCase):
|
||||
# Indexes are lazy so use list() to perform query
|
||||
list(BlogPost.objects)
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
||||
in info.values())
|
||||
self.assertTrue([('_types', 1), ('addDate', -1)] in info.values())
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
||||
in info)
|
||||
self.assertTrue([('_types', 1), ('addDate', -1)] in info)
|
||||
# tags is a list field so it shouldn't have _types in the index
|
||||
self.assertTrue([('tags', 1)] in info.values())
|
||||
|
||||
self.assertTrue([('tags', 1)] in info)
|
||||
|
||||
class ExtendedBlogPost(BlogPost):
|
||||
title = StringField()
|
||||
meta = {'indexes': ['title']}
|
||||
@@ -273,10 +311,11 @@ class DocumentTest(unittest.TestCase):
|
||||
|
||||
list(ExtendedBlogPost.objects)
|
||||
info = ExtendedBlogPost.objects._collection.index_information()
|
||||
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
||||
in info.values())
|
||||
self.assertTrue([('_types', 1), ('addDate', -1)] in info.values())
|
||||
self.assertTrue([('_types', 1), ('title', 1)] in info.values())
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
||||
in info)
|
||||
self.assertTrue([('_types', 1), ('addDate', -1)] in info)
|
||||
self.assertTrue([('_types', 1), ('title', 1)] in info)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
@@ -339,17 +378,34 @@ class DocumentTest(unittest.TestCase):
|
||||
class EmailUser(User):
|
||||
email = StringField(primary_key=True)
|
||||
self.assertRaises(ValueError, define_invalid_user)
|
||||
|
||||
|
||||
class EmailUser(User):
|
||||
email = StringField()
|
||||
|
||||
user = User(username='test', name='test user')
|
||||
user.save()
|
||||
|
||||
user_obj = User.objects.first()
|
||||
self.assertEqual(user_obj.id, 'test')
|
||||
self.assertEqual(user_obj.pk, 'test')
|
||||
|
||||
user_son = User.objects._collection.find_one()
|
||||
self.assertEqual(user_son['_id'], 'test')
|
||||
self.assertTrue('username' not in user_son['_id'])
|
||||
|
||||
|
||||
User.drop_collection()
|
||||
|
||||
user = User(pk='mongo', name='mongo user')
|
||||
user.save()
|
||||
|
||||
user_obj = User.objects.first()
|
||||
self.assertEqual(user_obj.id, 'mongo')
|
||||
self.assertEqual(user_obj.pk, 'mongo')
|
||||
|
||||
user_son = User.objects._collection.find_one()
|
||||
self.assertEqual(user_son['_id'], 'mongo')
|
||||
self.assertTrue('username' not in user_son['_id'])
|
||||
|
||||
User.drop_collection()
|
||||
|
||||
def test_creation(self):
|
||||
@@ -402,18 +458,18 @@ class DocumentTest(unittest.TestCase):
|
||||
"""
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField()
|
||||
|
||||
|
||||
self.assertTrue('content' in Comment._fields)
|
||||
self.assertFalse('id' in Comment._fields)
|
||||
self.assertFalse('collection' in Comment._meta)
|
||||
|
||||
|
||||
def test_embedded_document_validation(self):
|
||||
"""Ensure that embedded documents may be validated.
|
||||
"""
|
||||
class Comment(EmbeddedDocument):
|
||||
date = DateTimeField()
|
||||
content = StringField(required=True)
|
||||
|
||||
|
||||
comment = Comment()
|
||||
self.assertRaises(ValidationError, comment.validate)
|
||||
|
||||
@@ -438,6 +494,16 @@ class DocumentTest(unittest.TestCase):
|
||||
self.assertEqual(person_obj['name'], 'Test User')
|
||||
self.assertEqual(person_obj['age'], 30)
|
||||
self.assertEqual(person_obj['_id'], person.id)
|
||||
# Test skipping validation on save
|
||||
class Recipient(Document):
|
||||
email = EmailField(required=True)
|
||||
|
||||
recipient = Recipient(email='root@localhost')
|
||||
self.assertRaises(ValidationError, recipient.save)
|
||||
try:
|
||||
recipient.save(validate=False)
|
||||
except ValidationError:
|
||||
fail()
|
||||
|
||||
def test_delete(self):
|
||||
"""Ensure that document may be deleted using the delete method.
|
||||
@@ -452,7 +518,7 @@ class DocumentTest(unittest.TestCase):
|
||||
"""Ensure that a document may be saved with a custom _id.
|
||||
"""
|
||||
# Create person object and save it to the database
|
||||
person = self.Person(name='Test User', age=30,
|
||||
person = self.Person(name='Test User', age=30,
|
||||
id='497ce96f395f2f052a494fd4')
|
||||
person.save()
|
||||
# Ensure that the object is in the database with the correct _id
|
||||
@@ -460,6 +526,18 @@ class DocumentTest(unittest.TestCase):
|
||||
person_obj = collection.find_one({'name': 'Test User'})
|
||||
self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4')
|
||||
|
||||
def test_save_custom_pk(self):
|
||||
"""Ensure that a document may be saved with a custom _id using pk alias.
|
||||
"""
|
||||
# Create person object and save it to the database
|
||||
person = self.Person(name='Test User', age=30,
|
||||
pk='497ce96f395f2f052a494fd4')
|
||||
person.save()
|
||||
# Ensure that the object is in the database with the correct _id
|
||||
collection = self.db[self.Person._meta['collection']]
|
||||
person_obj = collection.find_one({'name': 'Test User'})
|
||||
self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4')
|
||||
|
||||
def test_save_list(self):
|
||||
"""Ensure that a list field may be properly saved.
|
||||
"""
|
||||
@@ -488,7 +566,7 @@ class DocumentTest(unittest.TestCase):
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_save_embedded_document(self):
|
||||
"""Ensure that a document with an embedded document field may be
|
||||
"""Ensure that a document with an embedded document field may be
|
||||
saved in the database.
|
||||
"""
|
||||
class EmployeeDetails(EmbeddedDocument):
|
||||
@@ -514,7 +592,7 @@ class DocumentTest(unittest.TestCase):
|
||||
def test_save_reference(self):
|
||||
"""Ensure that a document reference field may be saved in the database.
|
||||
"""
|
||||
|
||||
|
||||
class BlogPost(Document):
|
||||
meta = {'collection': 'blogpost_1'}
|
||||
content = StringField()
|
||||
@@ -533,8 +611,8 @@ class DocumentTest(unittest.TestCase):
|
||||
post_obj = BlogPost.objects.first()
|
||||
|
||||
# Test laziness
|
||||
self.assertTrue(isinstance(post_obj._data['author'],
|
||||
pymongo.dbref.DBRef))
|
||||
self.assertTrue(isinstance(post_obj._data['author'],
|
||||
bson.dbref.DBRef))
|
||||
self.assertTrue(isinstance(post_obj.author, self.Person))
|
||||
self.assertEqual(post_obj.author.name, 'Test User')
|
||||
|
||||
|
247
tests/fields.py
247
tests/fields.py
@@ -3,6 +3,7 @@ import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pymongo
|
||||
import gridfs
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import _get_db
|
||||
@@ -136,12 +137,16 @@ class FieldTest(unittest.TestCase):
|
||||
height = DecimalField(min_value=Decimal('0.1'),
|
||||
max_value=Decimal('3.5'))
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
person = Person()
|
||||
person.height = Decimal('1.89')
|
||||
person.validate()
|
||||
person.save()
|
||||
person.reload()
|
||||
self.assertEqual(person.height, Decimal('1.89'))
|
||||
|
||||
person.height = '2.0'
|
||||
person.validate()
|
||||
person.save()
|
||||
person.height = 0.01
|
||||
self.assertRaises(ValidationError, person.validate)
|
||||
person.height = Decimal('0.01')
|
||||
@@ -149,6 +154,8 @@ class FieldTest(unittest.TestCase):
|
||||
person.height = Decimal('4.0')
|
||||
self.assertRaises(ValidationError, person.validate)
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
def test_boolean_validation(self):
|
||||
"""Ensure that invalid values cannot be assigned to boolean fields.
|
||||
"""
|
||||
@@ -182,6 +189,9 @@ class FieldTest(unittest.TestCase):
|
||||
def test_list_validation(self):
|
||||
"""Ensure that a list field only accepts lists with valid elements.
|
||||
"""
|
||||
class User(Document):
|
||||
pass
|
||||
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField()
|
||||
|
||||
@@ -189,6 +199,7 @@ class FieldTest(unittest.TestCase):
|
||||
content = StringField()
|
||||
comments = ListField(EmbeddedDocumentField(Comment))
|
||||
tags = ListField(StringField())
|
||||
authors = ListField(ReferenceField(User))
|
||||
|
||||
post = BlogPost(content='Went for a walk today...')
|
||||
post.validate()
|
||||
@@ -203,15 +214,53 @@ class FieldTest(unittest.TestCase):
|
||||
post.tags = ('fun', 'leisure')
|
||||
post.validate()
|
||||
|
||||
comments = [Comment(content='Good for you'), Comment(content='Yay.')]
|
||||
post.comments = comments
|
||||
post.validate()
|
||||
|
||||
post.comments = ['a']
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
post.comments = 'yay'
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
|
||||
comments = [Comment(content='Good for you'), Comment(content='Yay.')]
|
||||
post.comments = comments
|
||||
post.validate()
|
||||
|
||||
post.authors = [Comment()]
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
|
||||
post.authors = [User()]
|
||||
post.validate()
|
||||
|
||||
def test_sorted_list_sorting(self):
|
||||
"""Ensure that a sorted list field properly sorts values.
|
||||
"""
|
||||
class Comment(EmbeddedDocument):
|
||||
order = IntField()
|
||||
content = StringField()
|
||||
|
||||
class BlogPost(Document):
|
||||
content = StringField()
|
||||
comments = SortedListField(EmbeddedDocumentField(Comment),
|
||||
ordering='order')
|
||||
tags = SortedListField(StringField())
|
||||
|
||||
post = BlogPost(content='Went for a walk today...')
|
||||
post.save()
|
||||
|
||||
post.tags = ['leisure', 'fun']
|
||||
post.save()
|
||||
post.reload()
|
||||
self.assertEqual(post.tags, ['fun', 'leisure'])
|
||||
|
||||
comment1 = Comment(content='Good for you', order=1)
|
||||
comment2 = Comment(content='Yay.', order=0)
|
||||
comments = [comment1, comment2]
|
||||
post.comments = comments
|
||||
post.save()
|
||||
post.reload()
|
||||
self.assertEqual(post.comments[0].content, comment2.content)
|
||||
self.assertEqual(post.comments[1].content, comment1.content)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_dict_validation(self):
|
||||
"""Ensure that dict types work as expected.
|
||||
"""
|
||||
@@ -356,14 +405,54 @@ class FieldTest(unittest.TestCase):
|
||||
class Employee(Document):
|
||||
name = StringField()
|
||||
boss = ReferenceField('self')
|
||||
friends = ListField(ReferenceField('self'))
|
||||
|
||||
bill = Employee(name='Bill Lumbergh')
|
||||
bill.save()
|
||||
peter = Employee(name='Peter Gibbons', boss=bill)
|
||||
|
||||
michael = Employee(name='Michael Bolton')
|
||||
michael.save()
|
||||
|
||||
samir = Employee(name='Samir Nagheenanajar')
|
||||
samir.save()
|
||||
|
||||
friends = [michael, samir]
|
||||
peter = Employee(name='Peter Gibbons', boss=bill, friends=friends)
|
||||
peter.save()
|
||||
|
||||
peter = Employee.objects.with_id(peter.id)
|
||||
self.assertEqual(peter.boss, bill)
|
||||
self.assertEqual(peter.friends, friends)
|
||||
|
||||
def test_recursive_embedding(self):
|
||||
"""Ensure that EmbeddedDocumentFields can contain their own documents.
|
||||
"""
|
||||
class Tree(Document):
|
||||
name = StringField()
|
||||
children = ListField(EmbeddedDocumentField('TreeNode'))
|
||||
|
||||
class TreeNode(EmbeddedDocument):
|
||||
name = StringField()
|
||||
children = ListField(EmbeddedDocumentField('self'))
|
||||
|
||||
tree = Tree(name="Tree")
|
||||
|
||||
first_child = TreeNode(name="Child 1")
|
||||
tree.children.append(first_child)
|
||||
|
||||
second_child = TreeNode(name="Child 2")
|
||||
first_child.children.append(second_child)
|
||||
|
||||
third_child = TreeNode(name="Child 3")
|
||||
first_child.children.append(third_child)
|
||||
|
||||
tree.save()
|
||||
|
||||
tree_obj = Tree.objects.first()
|
||||
self.assertEqual(len(tree.children), 1)
|
||||
self.assertEqual(tree.children[0].name, first_child.name)
|
||||
self.assertEqual(tree.children[0].children[0].name, second_child.name)
|
||||
self.assertEqual(tree.children[0].children[1].name, third_child.name)
|
||||
|
||||
def test_undefined_reference(self):
|
||||
"""Ensure that ReferenceFields may reference undefined Documents.
|
||||
@@ -551,5 +640,149 @@ class FieldTest(unittest.TestCase):
|
||||
AttachmentRequired.drop_collection()
|
||||
AttachmentSizeLimit.drop_collection()
|
||||
|
||||
def test_choices_validation(self):
|
||||
"""Ensure that value is in a container of allowed values.
|
||||
"""
|
||||
class Shirt(Document):
|
||||
size = StringField(max_length=3, choices=('S','M','L','XL','XXL'))
|
||||
|
||||
Shirt.drop_collection()
|
||||
|
||||
shirt = Shirt()
|
||||
shirt.validate()
|
||||
|
||||
shirt.size = "S"
|
||||
shirt.validate()
|
||||
|
||||
shirt.size = "XS"
|
||||
self.assertRaises(ValidationError, shirt.validate)
|
||||
|
||||
Shirt.drop_collection()
|
||||
|
||||
def test_file_fields(self):
|
||||
"""Ensure that file fields can be written to and their data retrieved
|
||||
"""
|
||||
class PutFile(Document):
|
||||
file = FileField()
|
||||
|
||||
class StreamFile(Document):
|
||||
file = FileField()
|
||||
|
||||
class SetFile(Document):
|
||||
file = FileField()
|
||||
|
||||
text = 'Hello, World!'
|
||||
more_text = 'Foo Bar'
|
||||
content_type = 'text/plain'
|
||||
|
||||
PutFile.drop_collection()
|
||||
StreamFile.drop_collection()
|
||||
SetFile.drop_collection()
|
||||
|
||||
putfile = PutFile()
|
||||
putfile.file.put(text, content_type=content_type)
|
||||
putfile.save()
|
||||
putfile.validate()
|
||||
result = PutFile.objects.first()
|
||||
self.assertTrue(putfile == result)
|
||||
self.assertEquals(result.file.read(), text)
|
||||
self.assertEquals(result.file.content_type, content_type)
|
||||
result.file.delete() # Remove file from GridFS
|
||||
|
||||
streamfile = StreamFile()
|
||||
streamfile.file.new_file(content_type=content_type)
|
||||
streamfile.file.write(text)
|
||||
streamfile.file.write(more_text)
|
||||
streamfile.file.close()
|
||||
streamfile.save()
|
||||
streamfile.validate()
|
||||
result = StreamFile.objects.first()
|
||||
self.assertTrue(streamfile == result)
|
||||
self.assertEquals(result.file.read(), text + more_text)
|
||||
self.assertEquals(result.file.content_type, content_type)
|
||||
result.file.delete()
|
||||
|
||||
# Ensure deleted file returns None
|
||||
self.assertTrue(result.file.read() == None)
|
||||
|
||||
setfile = SetFile()
|
||||
setfile.file = text
|
||||
setfile.save()
|
||||
setfile.validate()
|
||||
result = SetFile.objects.first()
|
||||
self.assertTrue(setfile == result)
|
||||
self.assertEquals(result.file.read(), text)
|
||||
|
||||
# Try replacing file with new one
|
||||
result.file.replace(more_text)
|
||||
result.save()
|
||||
result.validate()
|
||||
result = SetFile.objects.first()
|
||||
self.assertTrue(setfile == result)
|
||||
self.assertEquals(result.file.read(), more_text)
|
||||
result.file.delete()
|
||||
|
||||
PutFile.drop_collection()
|
||||
StreamFile.drop_collection()
|
||||
SetFile.drop_collection()
|
||||
|
||||
# Make sure FileField is optional and not required
|
||||
class DemoFile(Document):
|
||||
file = FileField()
|
||||
d = DemoFile.objects.create()
|
||||
|
||||
def test_file_uniqueness(self):
|
||||
"""Ensure that each instance of a FileField is unique
|
||||
"""
|
||||
class TestFile(Document):
|
||||
name = StringField()
|
||||
file = FileField()
|
||||
|
||||
# First instance
|
||||
testfile = TestFile()
|
||||
testfile.name = "Hello, World!"
|
||||
testfile.file.put('Hello, World!')
|
||||
testfile.save()
|
||||
|
||||
# Second instance
|
||||
testfiledupe = TestFile()
|
||||
data = testfiledupe.file.read() # Should be None
|
||||
|
||||
self.assertTrue(testfile.name != testfiledupe.name)
|
||||
self.assertTrue(testfile.file.read() != data)
|
||||
|
||||
TestFile.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_ensure_unique_default_instances(self):
|
||||
"""Ensure that every field has it's own unique default instance."""
|
||||
class D(Document):
|
||||
data = DictField()
|
||||
data2 = DictField(default=lambda: {})
|
||||
|
||||
d1 = D()
|
||||
d1.data['foo'] = 'bar'
|
||||
d1.data2['foo'] = 'bar'
|
||||
d2 = D()
|
||||
self.assertEqual(d2.data, {})
|
||||
self.assertEqual(d2.data2, {})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
import unittest
|
||||
import pymongo
|
||||
import bson
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from mongoengine.queryset import (QuerySet, MultipleObjectsReturned,
|
||||
@@ -53,15 +54,12 @@ class QuerySetTest(unittest.TestCase):
|
||||
person2 = self.Person(name="User B", age=30)
|
||||
person2.save()
|
||||
|
||||
q1 = Q(name='test')
|
||||
q2 = Q(age__gte=18)
|
||||
|
||||
# Find all people in the collection
|
||||
people = self.Person.objects
|
||||
self.assertEqual(len(people), 2)
|
||||
results = list(people)
|
||||
self.assertTrue(isinstance(results[0], self.Person))
|
||||
self.assertTrue(isinstance(results[0].id, (pymongo.objectid.ObjectId,
|
||||
self.assertTrue(isinstance(results[0].id, (bson.objectid.ObjectId,
|
||||
str, unicode)))
|
||||
self.assertEqual(results[0].name, "User A")
|
||||
self.assertEqual(results[0].age, 20)
|
||||
@@ -147,6 +145,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
"""
|
||||
# Try retrieving when no objects exists
|
||||
self.assertRaises(DoesNotExist, self.Person.objects.get)
|
||||
self.assertRaises(self.Person.DoesNotExist, self.Person.objects.get)
|
||||
|
||||
person1 = self.Person(name="User A", age=20)
|
||||
person1.save()
|
||||
@@ -155,6 +154,8 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
# Retrieve the first person from the database
|
||||
self.assertRaises(MultipleObjectsReturned, self.Person.objects.get)
|
||||
self.assertRaises(self.Person.MultipleObjectsReturned,
|
||||
self.Person.objects.get)
|
||||
|
||||
# Use a query to filter the people found to just person2
|
||||
person = self.Person.objects.get(age=30)
|
||||
@@ -163,6 +164,50 @@ class QuerySetTest(unittest.TestCase):
|
||||
person = self.Person.objects.get(age__lt=30)
|
||||
self.assertEqual(person.name, "User A")
|
||||
|
||||
def test_find_array_position(self):
|
||||
"""Ensure that query by array position works.
|
||||
"""
|
||||
class Comment(EmbeddedDocument):
|
||||
name = StringField()
|
||||
|
||||
class Post(EmbeddedDocument):
|
||||
comments = ListField(EmbeddedDocumentField(Comment))
|
||||
|
||||
class Blog(Document):
|
||||
tags = ListField(StringField())
|
||||
posts = ListField(EmbeddedDocumentField(Post))
|
||||
|
||||
Blog.drop_collection()
|
||||
|
||||
Blog.objects.create(tags=['a', 'b'])
|
||||
self.assertEqual(len(Blog.objects(tags__0='a')), 1)
|
||||
self.assertEqual(len(Blog.objects(tags__0='b')), 0)
|
||||
self.assertEqual(len(Blog.objects(tags__1='a')), 0)
|
||||
self.assertEqual(len(Blog.objects(tags__1='b')), 1)
|
||||
|
||||
Blog.drop_collection()
|
||||
|
||||
comment1 = Comment(name='testa')
|
||||
comment2 = Comment(name='testb')
|
||||
post1 = Post(comments=[comment1, comment2])
|
||||
post2 = Post(comments=[comment2, comment2])
|
||||
blog1 = Blog.objects.create(posts=[post1, post2])
|
||||
blog2 = Blog.objects.create(posts=[post2, post1])
|
||||
|
||||
blog = Blog.objects(posts__0__comments__0__name='testa').get()
|
||||
self.assertEqual(blog, blog1)
|
||||
|
||||
query = Blog.objects(posts__1__comments__1__name='testb')
|
||||
self.assertEqual(len(query), 2)
|
||||
|
||||
query = Blog.objects(posts__1__comments__1__name='testa')
|
||||
self.assertEqual(len(query), 0)
|
||||
|
||||
query = Blog.objects(posts__0__comments__1__name='testa')
|
||||
self.assertEqual(len(query), 0)
|
||||
|
||||
Blog.drop_collection()
|
||||
|
||||
def test_get_or_create(self):
|
||||
"""Ensure that ``get_or_create`` returns one result or creates a new
|
||||
document.
|
||||
@@ -175,16 +220,22 @@ class QuerySetTest(unittest.TestCase):
|
||||
# Retrieve the first person from the database
|
||||
self.assertRaises(MultipleObjectsReturned,
|
||||
self.Person.objects.get_or_create)
|
||||
self.assertRaises(self.Person.MultipleObjectsReturned,
|
||||
self.Person.objects.get_or_create)
|
||||
|
||||
# Use a query to filter the people found to just person2
|
||||
person = self.Person.objects.get_or_create(age=30)
|
||||
person, created = self.Person.objects.get_or_create(age=30)
|
||||
self.assertEqual(person.name, "User B")
|
||||
self.assertEqual(created, False)
|
||||
|
||||
person = self.Person.objects.get_or_create(age__lt=30)
|
||||
person, created = self.Person.objects.get_or_create(age__lt=30)
|
||||
self.assertEqual(person.name, "User A")
|
||||
self.assertEqual(created, False)
|
||||
|
||||
# Try retrieving when no objects exists - new doc should be created
|
||||
self.Person.objects.get_or_create(age=50, defaults={'name': 'User C'})
|
||||
kwargs = dict(age=50, defaults={'name': 'User C'})
|
||||
person, created = self.Person.objects.get_or_create(**kwargs)
|
||||
self.assertEqual(created, True)
|
||||
|
||||
person = self.Person.objects.get(age=50)
|
||||
self.assertEqual(person.name, "User C")
|
||||
@@ -255,6 +306,49 @@ class QuerySetTest(unittest.TestCase):
|
||||
obj = self.Person.objects(Q(name__iendswith='rossuM')).first()
|
||||
self.assertEqual(obj, person)
|
||||
|
||||
# Test exact
|
||||
obj = self.Person.objects(name__exact='Guido van Rossum').first()
|
||||
self.assertEqual(obj, person)
|
||||
obj = self.Person.objects(name__exact='Guido van rossum').first()
|
||||
self.assertEqual(obj, None)
|
||||
obj = self.Person.objects(name__exact='Guido van Rossu').first()
|
||||
self.assertEqual(obj, None)
|
||||
obj = self.Person.objects(Q(name__exact='Guido van Rossum')).first()
|
||||
self.assertEqual(obj, person)
|
||||
obj = self.Person.objects(Q(name__exact='Guido van rossum')).first()
|
||||
self.assertEqual(obj, None)
|
||||
obj = self.Person.objects(Q(name__exact='Guido van Rossu')).first()
|
||||
self.assertEqual(obj, None)
|
||||
|
||||
# Test iexact
|
||||
obj = self.Person.objects(name__iexact='gUIDO VAN rOSSUM').first()
|
||||
self.assertEqual(obj, person)
|
||||
obj = self.Person.objects(name__iexact='gUIDO VAN rOSSU').first()
|
||||
self.assertEqual(obj, None)
|
||||
obj = self.Person.objects(Q(name__iexact='gUIDO VAN rOSSUM')).first()
|
||||
self.assertEqual(obj, person)
|
||||
obj = self.Person.objects(Q(name__iexact='gUIDO VAN rOSSU')).first()
|
||||
self.assertEqual(obj, None)
|
||||
|
||||
# Test unsafe expressions
|
||||
person = self.Person(name='Guido van Rossum [.\'Geek\']')
|
||||
person.save()
|
||||
|
||||
obj = self.Person.objects(Q(name__icontains='[.\'Geek')).first()
|
||||
self.assertEqual(obj, person)
|
||||
|
||||
def test_not(self):
|
||||
"""Ensure that the __not operator works as expected.
|
||||
"""
|
||||
alice = self.Person(name='Alice', age=25)
|
||||
alice.save()
|
||||
|
||||
obj = self.Person.objects(name__iexact='alice').first()
|
||||
self.assertEqual(obj, alice)
|
||||
|
||||
obj = self.Person.objects(name__not__iexact='alice').first()
|
||||
self.assertEqual(obj, None)
|
||||
|
||||
def test_filter_chaining(self):
|
||||
"""Ensure filters can be chained together.
|
||||
"""
|
||||
@@ -464,11 +558,28 @@ class QuerySetTest(unittest.TestCase):
|
||||
obj = self.Person.objects(Q(name=re.compile('^gui', re.I))).first()
|
||||
self.assertEqual(obj, person)
|
||||
|
||||
obj = self.Person.objects(Q(name__ne=re.compile('^bob'))).first()
|
||||
obj = self.Person.objects(Q(name__not=re.compile('^bob'))).first()
|
||||
self.assertEqual(obj, person)
|
||||
obj = self.Person.objects(Q(name__ne=re.compile('^Gui'))).first()
|
||||
|
||||
obj = self.Person.objects(Q(name__not=re.compile('^Gui'))).first()
|
||||
self.assertEqual(obj, None)
|
||||
|
||||
def test_q_lists(self):
|
||||
"""Ensure that Q objects query ListFields correctly.
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
tags = ListField(StringField())
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
BlogPost(tags=['python', 'mongo']).save()
|
||||
BlogPost(tags=['python']).save()
|
||||
|
||||
self.assertEqual(len(BlogPost.objects(Q(tags='mongo'))), 1)
|
||||
self.assertEqual(len(BlogPost.objects(Q(tags='python'))), 2)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_exec_js_query(self):
|
||||
"""Ensure that queries are properly formed for use in exec_js.
|
||||
"""
|
||||
@@ -521,7 +632,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
class BlogPost(Document):
|
||||
name = StringField(db_field='doc-name')
|
||||
comments = ListField(EmbeddedDocumentField(Comment),
|
||||
comments = ListField(EmbeddedDocumentField(Comment),
|
||||
db_field='cmnts')
|
||||
|
||||
BlogPost.drop_collection()
|
||||
@@ -614,8 +725,33 @@ class QuerySetTest(unittest.TestCase):
|
||||
post.reload()
|
||||
self.assertTrue('db' in post.tags and 'nosql' in post.tags)
|
||||
|
||||
tags = post.tags[:-1]
|
||||
BlogPost.objects.update(pop__tags=1)
|
||||
post.reload()
|
||||
self.assertEqual(post.tags, tags)
|
||||
|
||||
BlogPost.objects.update_one(add_to_set__tags='unique')
|
||||
BlogPost.objects.update_one(add_to_set__tags='unique')
|
||||
post.reload()
|
||||
self.assertEqual(post.tags.count('unique'), 1)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_update_pull(self):
|
||||
"""Ensure that the 'pull' update operation works correctly.
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
slug = StringField()
|
||||
tags = ListField(StringField())
|
||||
|
||||
post = BlogPost(slug="test", tags=['code', 'mongodb', 'code'])
|
||||
post.save()
|
||||
|
||||
BlogPost.objects(slug="test").update(pull__tags="code")
|
||||
post.reload()
|
||||
self.assertTrue('code' not in post.tags)
|
||||
self.assertEqual(len(post.tags), 1)
|
||||
|
||||
def test_order_by(self):
|
||||
"""Ensure that QuerySets may be ordered.
|
||||
"""
|
||||
@@ -667,7 +803,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
"""
|
||||
|
||||
# run a map/reduce operation spanning all posts
|
||||
results = BlogPost.objects.map_reduce(map_f, reduce_f)
|
||||
results = BlogPost.objects.map_reduce(map_f, reduce_f, "myresults")
|
||||
results = list(results)
|
||||
self.assertEqual(len(results), 4)
|
||||
|
||||
@@ -678,7 +814,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertEqual(film.value, 3)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
|
||||
def test_map_reduce_with_custom_object_ids(self):
|
||||
"""Ensure that QuerySet.map_reduce works properly with custom
|
||||
primary keys.
|
||||
@@ -687,24 +823,24 @@ class QuerySetTest(unittest.TestCase):
|
||||
class BlogPost(Document):
|
||||
title = StringField(primary_key=True)
|
||||
tags = ListField(StringField())
|
||||
|
||||
|
||||
post1 = BlogPost(title="Post #1", tags=["mongodb", "mongoengine"])
|
||||
post2 = BlogPost(title="Post #2", tags=["django", "mongodb"])
|
||||
post3 = BlogPost(title="Post #3", tags=["hitchcock films"])
|
||||
|
||||
|
||||
post1.save()
|
||||
post2.save()
|
||||
post3.save()
|
||||
|
||||
|
||||
self.assertEqual(BlogPost._fields['title'].db_field, '_id')
|
||||
self.assertEqual(BlogPost._meta['id_field'], 'title')
|
||||
|
||||
|
||||
map_f = """
|
||||
function() {
|
||||
emit(this._id, 1);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# reduce to a list of tag ids and counts
|
||||
reduce_f = """
|
||||
function(key, values) {
|
||||
@@ -715,10 +851,10 @@ class QuerySetTest(unittest.TestCase):
|
||||
return total;
|
||||
}
|
||||
"""
|
||||
|
||||
results = BlogPost.objects.map_reduce(map_f, reduce_f)
|
||||
|
||||
results = BlogPost.objects.map_reduce(map_f, reduce_f, "myresults")
|
||||
results = list(results)
|
||||
|
||||
|
||||
self.assertEqual(results[0].object, post1)
|
||||
self.assertEqual(results[1].object, post2)
|
||||
self.assertEqual(results[2].object, post3)
|
||||
@@ -808,7 +944,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
finalize_f = """
|
||||
function(key, value) {
|
||||
// f(sec_since_epoch,y,z) =
|
||||
// f(sec_since_epoch,y,z) =
|
||||
// log10(z) + ((y*sec_since_epoch) / 45000)
|
||||
z_10 = Math.log(value.z) / Math.log(10);
|
||||
weight = z_10 + ((value.y * value.t_s) / 45000);
|
||||
@@ -827,6 +963,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
results = Link.objects.order_by("-value")
|
||||
results = results.map_reduce(map_f,
|
||||
reduce_f,
|
||||
"myresults",
|
||||
finalize_f=finalize_f,
|
||||
scope=scope)
|
||||
results = list(results)
|
||||
@@ -850,7 +987,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
BlogPost(hits=1, tags=['music', 'film', 'actors']).save()
|
||||
BlogPost(hits=2, tags=['music']).save()
|
||||
BlogPost(hits=3, tags=['music', 'actors']).save()
|
||||
BlogPost(hits=2, tags=['music', 'actors']).save()
|
||||
|
||||
f = BlogPost.objects.item_frequencies('tags')
|
||||
f = dict((key, int(val)) for key, val in f.items())
|
||||
@@ -872,16 +1009,26 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(f['actors'], 2.0/6.0)
|
||||
self.assertAlmostEqual(f['film'], 1.0/6.0)
|
||||
|
||||
# Check item_frequencies works for non-list fields
|
||||
f = BlogPost.objects.item_frequencies('hits')
|
||||
f = dict((key, int(val)) for key, val in f.items())
|
||||
self.assertEqual(set(['1', '2']), set(f.keys()))
|
||||
self.assertEqual(f['1'], 1)
|
||||
self.assertEqual(f['2'], 2)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_average(self):
|
||||
"""Ensure that field can be averaged correctly.
|
||||
"""
|
||||
self.Person(name='person', age=0).save()
|
||||
self.assertEqual(int(self.Person.objects.average('age')), 0)
|
||||
|
||||
ages = [23, 54, 12, 94, 27]
|
||||
for i, age in enumerate(ages):
|
||||
self.Person(name='test%s' % i, age=age).save()
|
||||
|
||||
avg = float(sum(ages)) / len(ages)
|
||||
avg = float(sum(ages)) / (len(ages) + 1) # take into account the 0
|
||||
self.assertAlmostEqual(int(self.Person.objects.average('age')), avg)
|
||||
|
||||
self.Person(name='ageless person').save()
|
||||
@@ -899,15 +1046,34 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.Person(name='ageless person').save()
|
||||
self.assertEqual(int(self.Person.objects.sum('age')), sum(ages))
|
||||
|
||||
def test_distinct(self):
|
||||
"""Ensure that the QuerySet.distinct method works.
|
||||
"""
|
||||
self.Person(name='Mr Orange', age=20).save()
|
||||
self.Person(name='Mr White', age=20).save()
|
||||
self.Person(name='Mr Orange', age=30).save()
|
||||
self.Person(name='Mr Pink', age=30).save()
|
||||
self.assertEqual(set(self.Person.objects.distinct('name')),
|
||||
set(['Mr Orange', 'Mr White', 'Mr Pink']))
|
||||
self.assertEqual(set(self.Person.objects.distinct('age')),
|
||||
set([20, 30]))
|
||||
self.assertEqual(set(self.Person.objects(age=30).distinct('name')),
|
||||
set(['Mr Orange', 'Mr Pink']))
|
||||
|
||||
def test_custom_manager(self):
|
||||
"""Ensure that custom QuerySetManager instances work as expected.
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
tags = ListField(StringField())
|
||||
deleted = BooleanField(default=False)
|
||||
|
||||
@queryset_manager
|
||||
def objects(doc_cls, queryset):
|
||||
return queryset(deleted=False)
|
||||
|
||||
@queryset_manager
|
||||
def music_posts(doc_cls, queryset):
|
||||
return queryset(tags='music')
|
||||
return queryset(tags='music', deleted=False)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
@@ -917,6 +1083,8 @@ class QuerySetTest(unittest.TestCase):
|
||||
post2.save()
|
||||
post3 = BlogPost(tags=['film', 'actors'])
|
||||
post3.save()
|
||||
post4 = BlogPost(tags=['film', 'actors'], deleted=True)
|
||||
post4.save()
|
||||
|
||||
self.assertEqual([p.id for p in BlogPost.objects],
|
||||
[post1.id, post2.id, post3.id])
|
||||
@@ -940,7 +1108,8 @@ class QuerySetTest(unittest.TestCase):
|
||||
BlogPost.drop_collection()
|
||||
|
||||
data = {'title': 'Post 1', 'comments': [Comment(content='test')]}
|
||||
BlogPost(**data).save()
|
||||
post = BlogPost(**data)
|
||||
post.save()
|
||||
|
||||
self.assertTrue('postTitle' in
|
||||
BlogPost.objects(title=data['title'])._query)
|
||||
@@ -948,12 +1117,33 @@ class QuerySetTest(unittest.TestCase):
|
||||
BlogPost.objects(title=data['title'])._query)
|
||||
self.assertEqual(len(BlogPost.objects(title=data['title'])), 1)
|
||||
|
||||
self.assertTrue('_id' in BlogPost.objects(pk=post.id)._query)
|
||||
self.assertEqual(len(BlogPost.objects(pk=post.id)), 1)
|
||||
|
||||
self.assertTrue('postComments.commentContent' in
|
||||
BlogPost.objects(comments__content='test')._query)
|
||||
self.assertEqual(len(BlogPost.objects(comments__content='test')), 1)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_query_pk_field_name(self):
|
||||
"""Ensure that the correct "primary key" field name is used when querying
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
title = StringField(primary_key=True, db_field='postTitle')
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
data = { 'title':'Post 1' }
|
||||
post = BlogPost(**data)
|
||||
post.save()
|
||||
|
||||
self.assertTrue('_id' in BlogPost.objects(pk=data['title'])._query)
|
||||
self.assertTrue('_id' in BlogPost.objects(title=data['title'])._query)
|
||||
self.assertEqual(len(BlogPost.objects(pk=data['title'])), 1)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_query_value_conversion(self):
|
||||
"""Ensure that query values are properly converted when necessary.
|
||||
"""
|
||||
@@ -1016,8 +1206,9 @@ class QuerySetTest(unittest.TestCase):
|
||||
# Indexes are lazy so use list() to perform query
|
||||
list(BlogPost.objects)
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
self.assertTrue([('_types', 1)] in info.values())
|
||||
self.assertTrue([('_types', 1), ('date', -1)] in info.values())
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('_types', 1)] in info)
|
||||
self.assertTrue([('_types', 1), ('date', -1)] in info)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
@@ -1032,6 +1223,29 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_dict_with_custom_baseclass(self):
|
||||
"""Ensure DictField working with custom base clases.
|
||||
"""
|
||||
class Test(Document):
|
||||
testdict = DictField()
|
||||
|
||||
t = Test(testdict={'f': 'Value'})
|
||||
t.save()
|
||||
|
||||
self.assertEqual(len(Test.objects(testdict__f__startswith='Val')), 0)
|
||||
self.assertEqual(len(Test.objects(testdict__f='Value')), 1)
|
||||
Test.drop_collection()
|
||||
|
||||
class Test(Document):
|
||||
testdict = DictField(basecls=StringField)
|
||||
|
||||
t = Test(testdict={'f': 'Value'})
|
||||
t.save()
|
||||
|
||||
self.assertEqual(len(Test.objects(testdict__f='Value')), 1)
|
||||
self.assertEqual(len(Test.objects(testdict__f__startswith='Val')), 1)
|
||||
Test.drop_collection()
|
||||
|
||||
def test_bulk(self):
|
||||
"""Ensure bulk querying by object id returns a proper dict.
|
||||
"""
|
||||
@@ -1070,40 +1284,238 @@ 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 miles 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 around 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 mile 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)
|
||||
|
||||
Event.drop_collection()
|
||||
|
||||
def test_custom_querysets(self):
|
||||
"""Ensure that custom QuerySet classes may be used.
|
||||
"""
|
||||
class CustomQuerySet(QuerySet):
|
||||
def not_empty(self):
|
||||
return len(self) > 0
|
||||
|
||||
class Post(Document):
|
||||
meta = {'queryset_class': CustomQuerySet}
|
||||
|
||||
Post.drop_collection()
|
||||
|
||||
self.assertTrue(isinstance(Post.objects, CustomQuerySet))
|
||||
self.assertFalse(Post.objects.not_empty())
|
||||
|
||||
Post().save()
|
||||
self.assertTrue(Post.objects.not_empty())
|
||||
|
||||
Post.drop_collection()
|
||||
|
||||
|
||||
class QTest(unittest.TestCase):
|
||||
|
||||
def test_or_and(self):
|
||||
"""Ensure that Q objects may be combined correctly.
|
||||
def test_empty_q(self):
|
||||
"""Ensure that empty Q objects won't hurt.
|
||||
"""
|
||||
q1 = Q(name='test')
|
||||
q1 = Q()
|
||||
q2 = Q(age__gte=18)
|
||||
q3 = Q()
|
||||
q4 = Q(name='test')
|
||||
q5 = Q()
|
||||
|
||||
query = ['(', {'name': 'test'}, '||', {'age__gte': 18}, ')']
|
||||
self.assertEqual((q1 | q2).query, query)
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
|
||||
query = ['(', {'name': 'test'}, '&&', {'age__gte': 18}, ')']
|
||||
self.assertEqual((q1 & q2).query, query)
|
||||
query = {'$or': [{'age': {'$gte': 18}}, {'name': 'test'}]}
|
||||
self.assertEqual((q1 | q2 | q3 | q4 | q5).to_query(Person), query)
|
||||
|
||||
query = ['(', '(', {'name': 'test'}, '&&', {'age__gte': 18}, ')', '||',
|
||||
{'name': 'example'}, ')']
|
||||
self.assertEqual((q1 & q2 | Q(name='example')).query, query)
|
||||
query = {'age': {'$gte': 18}, 'name': 'test'}
|
||||
self.assertEqual((q1 & q2 & q3 & q4 & q5).to_query(Person), query)
|
||||
|
||||
def test_item_query_as_js(self):
|
||||
"""Ensure that the _item_query_as_js utilitiy method works properly.
|
||||
def test_q_with_dbref(self):
|
||||
"""Ensure Q objects handle DBRefs correctly"""
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
class User(Document):
|
||||
pass
|
||||
|
||||
class Post(Document):
|
||||
created_user = ReferenceField(User)
|
||||
|
||||
user = User.objects.create()
|
||||
Post.objects.create(created_user=user)
|
||||
|
||||
self.assertEqual(Post.objects.filter(created_user=user).count(), 1)
|
||||
self.assertEqual(Post.objects.filter(Q(created_user=user)).count(), 1)
|
||||
|
||||
def test_and_combination(self):
|
||||
"""Ensure that Q-objects correctly AND together.
|
||||
"""
|
||||
q = Q()
|
||||
examples = [
|
||||
({'name': 'test'}, 'this.name == i0f0', {'i0f0': 'test'}),
|
||||
({'age': {'$gt': 18}}, 'this.age > i0f0o0', {'i0f0o0': 18}),
|
||||
({'name': 'test', 'age': {'$gt': 18, '$lte': 65}},
|
||||
'this.age <= i0f0o0 && this.age > i0f0o1 && this.name == i0f1',
|
||||
{'i0f0o0': 65, 'i0f0o1': 18, 'i0f1': 'test'}),
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
y = StringField()
|
||||
|
||||
# Check than an error is raised when conflicting queries are anded
|
||||
def invalid_combination():
|
||||
query = Q(x__lt=7) & Q(x__lt=3)
|
||||
query.to_query(TestDoc)
|
||||
self.assertRaises(InvalidQueryError, invalid_combination)
|
||||
|
||||
# Check normal cases work without an error
|
||||
query = Q(x__lt=7) & Q(x__gt=3)
|
||||
|
||||
q1 = Q(x__lt=7)
|
||||
q2 = Q(x__gt=3)
|
||||
query = (q1 & q2).to_query(TestDoc)
|
||||
self.assertEqual(query, {'x': {'$lt': 7, '$gt': 3}})
|
||||
|
||||
# More complex nested example
|
||||
query = Q(x__lt=100) & Q(y__ne='NotMyString')
|
||||
query &= Q(y__in=['a', 'b', 'c']) & Q(x__gt=-100)
|
||||
mongo_query = {
|
||||
'x': {'$lt': 100, '$gt': -100},
|
||||
'y': {'$ne': 'NotMyString', '$in': ['a', 'b', 'c']},
|
||||
}
|
||||
self.assertEqual(query.to_query(TestDoc), mongo_query)
|
||||
|
||||
def test_or_combination(self):
|
||||
"""Ensure that Q-objects correctly OR together.
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
|
||||
q1 = Q(x__lt=3)
|
||||
q2 = Q(x__gt=7)
|
||||
query = (q1 | q2).to_query(TestDoc)
|
||||
self.assertEqual(query, {
|
||||
'$or': [
|
||||
{'x': {'$lt': 3}},
|
||||
{'x': {'$gt': 7}},
|
||||
]
|
||||
})
|
||||
|
||||
def test_and_or_combination(self):
|
||||
"""Ensure that Q-objects handle ANDing ORed components.
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
y = BooleanField()
|
||||
|
||||
query = (Q(x__gt=0) | Q(x__exists=False))
|
||||
query &= Q(x__lt=100)
|
||||
self.assertEqual(query.to_query(TestDoc), {
|
||||
'$or': [
|
||||
{'x': {'$lt': 100, '$gt': 0}},
|
||||
{'x': {'$lt': 100, '$exists': False}},
|
||||
]
|
||||
})
|
||||
|
||||
q1 = (Q(x__gt=0) | Q(x__exists=False))
|
||||
q2 = (Q(x__lt=100) | Q(y=True))
|
||||
query = (q1 & q2).to_query(TestDoc)
|
||||
|
||||
self.assertEqual(['$or'], query.keys())
|
||||
conditions = [
|
||||
{'x': {'$lt': 100, '$gt': 0}},
|
||||
{'x': {'$lt': 100, '$exists': False}},
|
||||
{'x': {'$gt': 0}, 'y': True},
|
||||
{'x': {'$exists': False}, 'y': True},
|
||||
]
|
||||
for item, js, scope in examples:
|
||||
test_scope = {}
|
||||
self.assertEqual(q._item_query_as_js(item, test_scope, 0), js)
|
||||
self.assertEqual(scope, test_scope)
|
||||
self.assertEqual(len(conditions), len(query['$or']))
|
||||
for condition in conditions:
|
||||
self.assertTrue(condition in query['$or'])
|
||||
|
||||
def test_or_and_or_combination(self):
|
||||
"""Ensure that Q-objects handle ORing ANDed ORed components. :)
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
y = BooleanField()
|
||||
|
||||
q1 = (Q(x__gt=0) & (Q(y=True) | Q(y__exists=False)))
|
||||
q2 = (Q(x__lt=100) & (Q(y=False) | Q(y__exists=False)))
|
||||
query = (q1 | q2).to_query(TestDoc)
|
||||
|
||||
self.assertEqual(['$or'], query.keys())
|
||||
conditions = [
|
||||
{'x': {'$gt': 0}, 'y': True},
|
||||
{'x': {'$gt': 0}, 'y': {'$exists': False}},
|
||||
{'x': {'$lt': 100}, 'y':False},
|
||||
{'x': {'$lt': 100}, 'y': {'$exists': False}},
|
||||
]
|
||||
self.assertEqual(len(conditions), len(query['$or']))
|
||||
for condition in conditions:
|
||||
self.assertTrue(condition in query['$or'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Reference in New Issue
Block a user