Compare commits

...

169 Commits
v0.3 ... v0.4.1

Author SHA1 Message Date
Ross Lawley
3da37fbf6e Updated for pymongo 2012-09-10 18:24:37 +00:00
Harry Marr
69989365c7 Merge branch 'v0.4' 2010-10-18 13:56:07 +01:00
Harry Marr
2b9c526b47 Merge branch 'v0.4' of http://github.com/schallis/mongoengine into v0.4 2010-10-18 13:55:57 +01:00
Steve Challis
d7c42861fb Minor GridFS corrections 2010-10-18 10:25:06 +01:00
Harry Marr
e9d478ed9f Merge branch 'master' of http://github.com/cyberdelia/mongoengine into v0.4 2010-10-18 10:22:56 +01:00
Harry Marr
d6cb5b9abe Removed invalid connection tests 2010-10-18 10:21:23 +01:00
Steve Challis
5580b003b5 Added self to AUTHORS 2010-10-18 01:30:32 +01:00
Steve Challis
67736c849d Finished GridFS Documentation
* Also made GridFS replace test pass
2010-10-18 00:55:44 +01:00
Steve Challis
39e27735cc Merge branch 'v0.4' of git://github.com/hmarr/mongoengine into v0.4
Conflicts:
	docs/changelog.rst
	mongoengine/base.py
	mongoengine/queryset.py
2010-10-17 23:48:20 +01:00
Harry Marr
0902b95764 Added support for recursive embedded documents 2010-10-18 00:27:40 +01:00
Steve Challis
dc7181a3fd Begun GridFS documentation 2010-10-17 23:43:58 +01:00
Harry Marr
e93c4c87d8 Fixed inheritance collection issue 2010-10-17 17:41:20 +01:00
Harry Marr
dcec61e9b2 Raise AttributeError when necessary on QuerySet[] 2010-10-17 16:36:22 +01:00
Harry Marr
007f116bfa Increment version to 0.4 2010-10-17 15:42:31 +01:00
Harry Marr
6817f3b7ba Updated docs for v0.4 2010-10-17 15:40:49 +01:00
Harry Marr
36993029ad Removed old Q-object implementation 2010-10-17 14:22:45 +01:00
Harry Marr
012352cf24 Added snapshot and timeout methods to QuerySet 2010-10-17 14:21:55 +01:00
Harry Marr
26723992e3 Combined Q-object tests 2010-10-17 14:14:05 +01:00
Harry Marr
3591593ac7 Fixed GenericReferenceField query issue 2010-10-17 13:55:48 +01:00
Harry Marr
d3c2dfbaee Merge branch 'master' of http://github.com/ixc/mongoengine into v0.4
Conflicts:
	mongoengine/fields.py
	mongoengine/queryset.py
2010-10-17 13:54:16 +01:00
Harry Marr
b2b4456f74 Merge branch 'new-q-objects' into v0.4 2010-10-17 13:42:29 +01:00
Harry Marr
f666141981 Added test for list of referencefields 2010-10-17 13:23:11 +01:00
Harry Marr
fb4c4e3e08 Merge branch 'master' of http://github.com/jaimebuelta/mongoengine into v0.4 2010-10-17 13:06:41 +01:00
Harry Marr
34fa5cd241 Added some imports for PyMongo 1.9 compatibility. 2010-10-16 16:03:26 +01:00
Jaime
833fa3d94d Added note about the use of default parameters 2010-10-06 20:02:14 +01:00
Harry Marr
92471445ec Fix changing databases
Conflicts:

	mongoengine/connection.py
	mongoengine/queryset.py
2010-10-05 00:46:13 +01:00
Harry Marr
3acfd90720 Added some imports for PyMongo 1.9 compatibility. 2010-10-04 14:58:00 +01:00
Harry Marr
4742328b90 Delete stale cursor when query is filtered. Closes #62. 2010-10-04 12:11:01 +01:00
Harry Marr
b4c54b1b62 Added support for the $not operator 2010-10-04 11:41:49 +01:00
Harry Marr
76cb851c40 Replaced old Q-object with new, revamped Q-object 2010-10-04 11:41:07 +01:00
Harry Marr
3fcc0e9789 Combining OR nodes works, fixed other Q-object bugs 2010-10-04 02:10:37 +01:00
Harry Marr
8e65154201 Added a tree transformer, got complex ANDs working 2010-10-04 00:06:42 +01:00
Harry Marr
c0f7c4ca2d Fixed error in empty property on QCombination 2010-10-03 23:22:36 +01:00
Harry Marr
db2f64c290 Made query-tree code a bit clearer 2010-10-03 23:01:44 +01:00
Harry Marr
a3c46fec07 Compilation of combinations - simple $or now works 2010-10-03 21:26:26 +01:00
Harry Marr
62388cb740 Started work on new Q-object implementation 2010-10-03 21:08:28 +01:00
Timothée Peignier
9c9903664a add support for pk property in documents and filters 2010-10-03 18:50:35 +02:00
Harry Marr
556eed0151 QuerySet.distinct respects query. Closes #64. 2010-10-03 15:22:47 +01:00
Harry Marr
4012722a8d QuerySet.item_frequencies works with non-list fields 2010-10-03 15:01:45 +01:00
Harry Marr
4c68bc6c96 Merge branch 'master' of http://github.com/sibsibsib/mongoengine into v0.4
Conflicts:
	mongoengine/queryset.py
	tests/queryset.py
2010-10-03 01:57:22 +01:00
Harry Marr
159923fae2 Made lists of recursive reference fields possible 2010-10-03 01:48:42 +01:00
sib
72c7a010ff added unit test for addToSet 2010-09-30 03:05:15 -03:00
sib
2c8f004103 added update operator for addToSet 2010-09-30 02:53:44 -03:00
Steve Challis
67a9b358a0 Merge branch 'v0.4' of git://github.com/hmarr/mongoengine into v0.4 2010-09-29 23:39:09 +01:00
Steve Challis
b5eb3ea1cd Added a Django storage backend.
- New GridFSStorage storage backend
- New FileDocument document for storing files in GridFS
- Whitespace cleaned up in various files
2010-09-29 23:36:58 +01:00
Harry Marr
98bc0a7c10 Raise AttributeError when necessary on GridFSProxy 2010-09-25 22:47:09 +01:00
Harry Marr
bb24879149 Moved custom queryset test to correct place 2010-09-19 20:00:53 +01:00
Harry Marr
3d6ee0ce00 Merge branch 'master' of http://github.com/n1k0/mongoengine into v0.4 2010-09-19 19:55:57 +01:00
Harry Marr
ee72845701 Merge branch 'v0.4' of http://github.com/samuelclay/mongoengine into v0.4 2010-09-19 19:53:12 +01:00
Harry Marr
d158727154 Merge branch 'v0.4' of github.com:hmarr/mongoengine into v0.4 2010-09-19 17:18:39 +01:00
Harry Marr
73092dcb33 QuerySet update method returns num affected docs 2010-09-19 17:17:37 +01:00
Samuel Clay
91ddd310ba Merge branch 'v0.4' of github.com:samuelclay/mongoengine into v0.4 2010-09-16 17:21:21 -04:00
Samuel Clay
20dd7562e0 Adding multiprocessing support to mongoengine by using the identity of the process to define the connection. Each 'thread' gets its own pymongo connection. 2010-09-16 17:19:58 -04:00
Greg Turner
b7e84031e3 Escape strings for regex query. 2010-09-16 14:37:18 +10:00
Harry Marr
f11ee1f9cf Added support for using custom QuerySet classes 2010-09-15 09:47:13 +01:00
Nicolas Perriault
449f5a00dc added a 'validate' option to Document.save() +docs +tests 2010-09-11 17:45:57 +02:00
Steve Challis
bd1bf9ba24 Merge branch 'v0.4' of git://github.com/hmarr/mongoengine into v0.4
Conflicts:
	mongoengine/fields.py
	tests/fields.py
2010-08-31 00:25:10 +01:00
Harry Marr
2af5f3c56e Added support for querying by array position. Closes #36. 2010-08-31 00:24:30 +01:00
Harry Marr
1849f75ad0 Made GridFSProxy a bit stricter / safer 2010-08-31 00:23:59 +01:00
Harry Marr
32e66b29f4 Fixed FileField problem caused by shared objects 2010-08-30 22:12:05 +01:00
Harry Marr
69012e8ad1 Fixed incorrect $pull test 2010-08-30 19:59:49 +01:00
Harry Marr
17642c8a8c Fixed QuerySet.average issue that ignored 0 2010-08-30 19:48:17 +01:00
Mircea Pasoi
f1aec68f23 Inherit index options. 2010-08-30 17:40:01 +01:00
Mircea Pasoi
3e30d71263 Support for background and drop_dups indexing options. 2010-08-30 17:39:41 +01:00
Mircea Pasoi
266f33adc4 Bug fix for gridfs FileField. 2010-08-30 17:38:28 +01:00
Mircea Pasoi
dcc8d22cec Proper unique index name when using db_field. 2010-08-30 17:38:07 +01:00
Harry Marr
9540555b26 Merge branch 'master' of git://github.com/flosch/mongoengine into v0.4 2010-08-30 17:29:59 +01:00
Florian Schlachter
185e7a6a7e Better way of checking if new_class has an 'objects' attribute. 2010-08-30 18:38:41 +02:00
Florian Schlachter
c39f315ddc Merge remote branch 'hmarr/v0.4' 2010-08-30 18:38:12 +02:00
Harry Marr
5b230b90b9 Doc fix (all operator) 2010-08-30 15:34:29 +01:00
Harry Marr
1ed9a36d0a Added test for pop operation 2010-08-30 14:02:02 +01:00
Florian Schlachter
e0911a5fe0 Replaced slow exception handling with has_key. 2010-08-30 14:58:58 +02:00
Harry Marr
3297578e8d Merge branch 'master' of http://github.com/richardhenry/mongoengine into v0.4 2010-08-30 13:57:42 +01:00
Harry Marr
954d5c16d8 Merge branch 'master' of http://github.com/serverplot/mongoengine into v0.4 2010-08-30 13:56:28 +01:00
Florian Schlachter
95efa39b52 Added *.egg to .gitignore. 2010-08-30 14:56:18 +02:00
Florian Schlachter
c27ccc91d2 Merge remote branch 'hmarr/v0.4'
Conflicts:
	tests/fields.py
2010-08-30 14:55:49 +02:00
Harry Marr
4fb6fcabef Added test for overriding objects 2010-08-30 13:54:20 +01:00
Harry Marr
2635e41f69 Merge branch 'master' of http://github.com/theojulienne/mongoengine into v0.4 2010-08-30 13:54:07 +01:00
Harry Marr
ba01817ee3 Merge branch 'master' of http://github.com/vandersonmota/mongoengine into v0.4 2010-08-30 13:34:43 +01:00
Florian Schlachter
1e1d7073c8 Merge remote branch 'hmarr/v0.4'
Conflicts:
	tests/fields.py
2010-08-30 14:33:04 +02:00
Harry Marr
40eb23a97a Merge branch 'master' of git://github.com/flosch/mongoengine into v0.4
Conflicts:
	tests/fields.py
2010-08-30 13:21:10 +01:00
Harry Marr
f4711699e4 Merge branch 'master' of http://github.com/danielhasselrot/mongoengine into v0.4
Conflicts:
	mongoengine/fields.py
2010-08-30 13:03:23 +01:00
Harry Marr
3b62cf80cd Fixed {Dict,List}Field default issue. Closes #46. 2010-08-30 13:00:34 +01:00
Harry Marr
d99c5973c3 Fixed Q object DBRef test bug 2010-08-30 12:52:24 +01:00
Richard Henry
7de9adc6b1 Adding support for pop operations to QuerySet.update and QuerySet.update_one 2010-08-28 09:36:25 +01:00
sp
17addbefe2 made it more like Django's user model 2010-08-18 16:50:52 -04:00
Greg Turner
d274576b47 Fixed premature return for query gen 2010-08-13 22:30:36 +10:00
Greg Turner
6373e20696 Better error reporting on a validation error for a list. 2010-08-13 22:28:26 +10:00
Greg Turner
809fe44b43 Added a __raw__ parameter for passing query dictionaries directly to pymongo 2010-08-12 15:14:20 +10:00
Greg Turner
198ccc028a made list queries work with regexes (e.g. istartswith) 2010-08-06 20:29:09 +10:00
Theo Julienne
b96e27a7e4 Allow documents to override the 'objects' QuerySetManager 2010-07-30 22:09:00 +10:00
Timothée Peignier
1147ac4350 ignore virtualenv directory 2010-07-27 01:30:16 +08:00
flosch
21d267cb11 Now order_by() works like queries for referencing deeper fields (replacing . with __). old: order_by('mydoc.myattr') / new: order_by('mydoc__myattr'). Closes #45 2010-07-26 17:28:59 +02:00
flosch
6791f205af Style fix. 2010-07-26 16:50:09 +02:00
flosch
7ab2e21c10 Handle unsafe expressions when using startswith/endswith/contains with unsafe expressions. Closes #58 2010-07-26 16:42:10 +02:00
flosch
2f991ac6f1 Added all() method to get all document instances from a document. Extended the FileField's tests with testing on empty filefield. 2010-07-25 19:02:15 +02:00
flosch
9411b38508 Removed unnecessary comment. 2010-07-25 18:45:49 +02:00
flosch
386c48b116 Typo. 2010-07-25 18:43:11 +02:00
flosch
9d82911f63 Added tests for #46. 2010-07-25 18:38:24 +02:00
flosch
51065e7a4d Closes #46 by instantiating a new default instance for every field by request. 2010-07-25 18:33:33 +02:00
flosch
327452622e Handle DBRefs correctly within Q objects. Closes #55 2010-07-25 18:22:26 +02:00
flosch
13316e5380 Introduced new Document.objects.create, like django has. It creates a new object, saves it and returns the new object instance. 2010-07-25 17:35:09 +02:00
Harry Marr
9f98025b8c Added QuerySet.distinct. Closes #44. 2010-07-25 15:29:02 +01:00
Harry Marr
564f950037 Merge branch 'master' of git://github.com/flosch/mongoengine into v0.4 2010-07-25 15:09:45 +01:00
Harry Marr
be651caa68 Removed a couple of sneaky print statements 2010-07-25 15:02:37 +01:00
Florian Schlachter
aa00feb6a5 FileField's values are now optional. When no value is applied, no File object is created and referenced. 2010-07-20 22:46:00 +02:00
Florian Schlachter
03c0fd9ada Make default value of DictField an empty dict instead of None. 2010-07-19 19:01:53 +02:00
Daniel Hasselrot
6093e88eeb Made list store empty list by default 2010-07-19 08:07:03 +08:00
Florian Schlachter
ec519f20fa Makes the tests compatible to pymongo 1.7+. Not backwards compatible! 2010-07-19 01:32:28 +02:00
Florian Schlachter
d3495896fa Merge branch 'master' of github.com:flosch/mongoengine 2010-07-19 01:12:16 +02:00
Florian Schlachter
323c86308a Merge remote branch 'hmarr/v0.4'
Conflicts:
	mongoengine/fields.py
	tests/fields.py
2010-07-19 01:11:28 +02:00
martin
f9057e1a28 Fixed bug in FileField, proxy was not getting the grid_id set 2010-07-19 07:03:45 +08:00
Florian Schlachter
9596a25bb9 Fixed documentation bug. 2010-07-19 00:56:16 +02:00
Steve Challis
47bfeec115 Tidied code, added replace() method to FileField 2010-07-19 06:53:21 +08:00
martin
6bfd6c322b Fixed bug with GeoLocationField 2010-07-19 06:52:57 +08:00
Steve Challis
0512dd4c25 Added new FileField with GridFS support
The API is similar to that of PyMongo and most of the same operations are
possible.

The FileField can be written too with put(), write() or by using the assignment
operator. All three cases are demonstrated in the tests. Metadata can be added
to a FileField by assigning keyword arguments when using put() or new_file().
2010-07-19 06:52:11 +08:00
Daniel Hasselrot
acbc741037 Made list store empty list by default 2010-07-15 18:20:29 +02:00
Harry Marr
c2163ecee5 Added test for Geo indexes 2010-07-07 15:12:14 +01:00
Harry Marr
71689fcf23 Got within_box working for Geo fields 2010-07-07 15:00:46 +01:00
Harry Marr
1c334141ee Merge branch 'geo' of git://github.com/blackbrrr/mongoengine into v0.4
Conflicts:
	mongoengine/fields.py
	mongoengine/queryset.py
2010-07-07 14:53:25 +01:00
Daniel Hasselrot
b89d71bfa5 Do not convert None objects 2010-07-06 14:17:30 +02:00
Daniel Hasselrot
3179c4e4ac Now only removes _id if none, for real 2010-07-06 11:25:49 +02:00
Daniel Hasselrot
f5e39c0064 Allowed _id to be missing when converting to mongo 2010-07-06 10:25:31 +02:00
vandersonmota
86e2797c57 added a TestCase for tests that uses mongoDB 2010-06-09 22:28:30 -03:00
Steve Challis
39b749432a Tidied code, added replace() method to FileField 2010-06-03 08:27:21 +01:00
Steve Challis
0ad343484f Added new FileField with GridFS support
The API is similar to that of PyMongo and most of the same operations are
possible.

The FileField can be written too with put(), write() or by using the assignment
operator. All three cases are demonstrated in the tests. Metadata can be added
to a FileField by assigning keyword arguments when using put() or new_file().
2010-06-02 20:53:39 +01:00
Harry Marr
196606438c Fixed Q-object list query issue 2010-05-30 18:34:06 +01:00
Harry Marr
6896818bfd Added docs for exact, iexact 2010-05-30 17:40:01 +01:00
Flavio Amieiro
eb4f0ad7fb Merge branch 'master' of git://github.com/hmarr/mongoengine 2010-05-29 11:52:08 -03:00
Harry Marr
467e61bcc1 Documentation fix 2010-05-28 02:28:42 +01:00
Flavio Amieiro
a2c78c9063 Add 'exact' and 'iexact' match operators for QuerySets 2010-05-26 20:24:57 -03:00
Harry Marr
b23353e376 Fixed inherited document primary key issue 2010-05-24 23:03:30 +01:00
Harry Marr
b8e9790de3 Merge branch 'master' of git://github.com/flosch/mongoengine 2010-05-22 15:59:41 +01:00
Harry Marr
e37e8d9e65 Merge branch 'master' of git://github.com/blackbrrr/mongoengine 2010-05-22 15:53:54 +01:00
Stephan Jaekel
f657432be3 always ignore empty Q objects, if the new Q is empty, the old one will be returned. 2010-05-14 14:35:27 +02:00
Stephan Jaekel
80c2895e56 Merge branch 'master' of http://github.com/flosch/mongoengine 2010-05-14 14:22:29 +02:00
Stephan Jaekel
88da998532 added test for empty Q objects 2010-05-14 14:21:58 +02:00
Stephan Jaekel
225972e151 Added some handy shortcuts for django users. 2010-05-14 14:03:18 +02:00
Stephan Jaekel
4972bdb383 ignore empty Q objects when combining Q objects. 2010-05-14 14:02:39 +02:00
Florian Schlachter
11c7a15067 Added test for DictField's basecls. 2010-05-14 13:49:13 +02:00
Florian Schlachter
9df725165b Added a possibility to define a base class for fields from a DictField (instead of using BaseField). This is important if you want to use field-based query abilities like StringField's startswith/endswith/contains. Just define `basecls´ when defining your DictField. Example:
class Test(Document):

    name = StringField()
    translations = DictField(basecls=StringField)

Without basecls defined:

> Test.objects(translations__german__startswith='Deutsch')
[]

With basecls set to StringField:

> Test.objects(translations__german__startswith='Deutsch')
[<Test: Test object>]
2010-05-14 13:35:45 +02:00
Florian Schlachter
682326c130 documentation bug fixed 2010-04-30 18:04:58 +02:00
Matt Dennewitz
86575cb035 can't use unicode strings for __init__ kwargs 2010-04-19 09:39:03 -05:00
Florian Schlachter
eecc6188a7 fixes issue #41 since unicode kwargs is an feature of python 2.6.5 and above. 2010-04-19 11:34:09 +02:00
Harry Marr
3b4df4615a Fixed MRO error that occured on document inheritance 2010-04-17 21:45:11 +01:00
Harry Marr
edfda6ad5b BinaryField returns str not unicode 2010-04-17 21:24:06 +01:00
Florian Schlachter
3c7e8be2e7 Removed create_default since it can be achieved with the default argument (like default=MyEmbeddedDocument since default takes callables too). 2010-04-17 16:59:09 +02:00
Florian Schlachter
416fcba846 Merge remote branch 'hmarr/master'
Conflicts:
	mongoengine/base.py
2010-04-17 01:42:26 +02:00
Florian Schlachter
e196e229cd Accepting a tuple for validation argument. 2010-04-17 01:36:45 +02:00
Florian Schlachter
da57572409 Introduced new create_default field argument. If set to true, mongoengine will automagically create an instance of the desired document class (useful if using EmbeddedDocumentField for example):
class SubDoc(EmbeddedDocument):
        url = URLField()

    class MyDoc(Document):
        subdoc = EmbeddedDocumentField(SubDoc, create_default=True)

With create_default MyDoc().subdoc is automatically instantiated. Hint: default=SubDoc() WON'T work (that's why I've introduced create_default)
2010-04-17 01:23:14 +02:00
Florian Schlachter
ef172712da bugfix 2010-04-16 22:25:45 +02:00
Florian Schlachter
170c56bcb9 introduced min_length for a StringField 2010-04-16 18:13:11 +02:00
Florian Schlachter
f3ca9fa4c5 Make validation-lists possible. Example:
class Doc(Document):
        country = StringField(validation=['DE', 'AT', 'CH'])
2010-04-16 18:00:51 +02:00
Florian Schlachter
48facec524 Fixes tiny documentation error. Adds possibility to add custom validation methods to fields, e. g.:
class Customer(Document):
        country = StringField(validation=lambda value: value in ['DE', 'AT', 'CH'])

Replaced some str() with unicode() for i18n reasons.
2010-04-16 16:59:34 +02:00
Don Spaulding
ee0c75a26d Add choices keyword argument to BaseField.__init__() 2010-04-15 17:59:35 -05:00
Don Spaulding
e9c92f30ba Add description of each of the keyword arguments to BaseField.__init__(), adds description for choices too. 2010-04-15 17:57:23 -05:00
Florian Schlachter
0a074e52e0 Merge remote branch 'hmarr/master'
Conflicts:
	mongoengine/fields.py
2010-04-15 23:10:34 +02:00
Don Spaulding
da3f4c30e2 Fix doc typos 2010-04-14 22:40:56 -05:00
Harry Marr
2b08ca7c99 Merge branch 'SortedListField' of git://github.com/joshourisman/mongoengine 2010-04-12 17:41:09 +01:00
Josh Ourisman
c8e466a160 Moved SortedListField stuff into its own branch 2010-04-12 12:31:52 -04:00
Timothée Peignier
a39685d98c make get_or_create returns a tuple with the retrieved or created object and a boolean specifying whether a new object was created 2010-04-12 16:21:29 +01:00
Harry Marr
90200dbe9c Fixed DecimalField bug 2010-04-12 15:59:20 +01:00
Florian Schlachter
2304dac8e3 added GeoLocationField with auto index-creation for GEO2D 2010-03-30 00:04:39 +02:00
Florian Schlachter
38b2919c0d added emailfield 2010-03-29 22:02:33 +02:00
Deepak Thukral
207fd9fcb7 keeping import policy in mind 2010-03-29 11:27:50 +02:00
Deepak Thukral
fbcf58c48f updated documentation 2010-03-29 11:25:17 +02:00
Deepak Thukral
8f4a579df9 DoesNotExist and MultipleObjectsReturned now contributes Document class 2010-03-28 22:22:36 +02:00
Matt Dennewitz
600ca3bcf9 renamed 'test_near' to 'test_geospatial_operators', updated added ordering checks to test 2010-03-23 00:57:26 -05:00
Matt Dennewitz
a4d2f22fd2 added 'geo_indexes' to TopLevelDocumentMetaclass; added GeoPointField, a glorified [lat float, lng float] container; added geo lookup operators to QuerySet; added initial geo tests 2010-03-23 00:14:01 -05:00
24 changed files with 2218 additions and 408 deletions

4
.gitignore vendored
View File

@@ -1,7 +1,9 @@
*.pyc
.*.swp
*.egg
docs/.build
docs/_build
build/
dist/
mongoengine.egg-info/
mongoengine.egg-info/
env/

View File

@@ -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>

View File

@@ -64,3 +64,7 @@ Fields
.. autoclass:: mongoengine.ReferenceField
.. autoclass:: mongoengine.GenericReferenceField
.. autoclass:: mongoengine.FileField
.. autoclass:: mongoengine.GeoPointField

View File

@@ -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

View File

@@ -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

View File

@@ -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
========

View File

@@ -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
View 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')

View File

@@ -10,3 +10,4 @@ User Guide
defining-documents
document-instances
querying
gridfs

View File

@@ -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)

View File

@@ -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>`_.

View File

@@ -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])

View File

@@ -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})

View File

@@ -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)

View File

@@ -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 []

View 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

View 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

View 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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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')

View File

@@ -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()

View File

@@ -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()