Compare commits

...

400 Commits

Author SHA1 Message Date
Ross Lawley
3070e0bf5d Fix for inheritance bug and db_alias 2012-07-20 10:34:08 +01:00
Ross Lawley
83c11a9834 Version bump 2012-07-19 16:12:21 +01:00
Ross Lawley
5c912b930e Removed tests testing MongoDB not mongoengine 2012-07-19 16:03:29 +01:00
Ross Lawley
1b17fb0ae7 Updated validation error messages
refs hmarr/mongoengine#539
2012-07-19 15:04:12 +01:00
Ross Lawley
d83e67c121 Added support for null / zero / false values in item_frequencies
refs /MongoEngine/mongoengine#40
2012-07-19 12:08:07 +01:00
Ross Lawley
ae39ed94c9 Fixed cascade save edge case
refs MongoEngine/mongoengine#40
2012-07-19 11:52:26 +01:00
Ross Lawley
1e51180d42 Fixed geo index creation bug
fixes MongoEngine/mongoengine#36
2012-07-19 11:39:52 +01:00
Ross Lawley
87ba69d02e Updated changelog 2012-07-19 10:35:37 +01:00
Ross Lawley
8879d5560b Added support for args / kwargs and queryset_manager
Closes MongoEngine/mongoengine#37
2012-07-19 10:32:33 +01:00
Ross Lawley
c1621ee39c Merge pull request #39 from wpjunior/tests2
More one test
2012-07-18 06:10:26 -07:00
Ross Lawley
b0aa98edb4 Deref list custom id fix 2012-07-18 14:09:24 +01:00
Wilson Júnior
a7a2fe0216 added more tests 2012-07-18 06:37:23 -03:00
Ross Lawley
8e50f5fa3c Version bump 2012-07-11 16:59:24 +01:00
Ross Lawley
31793520bf Updated changelog / AUTHORS
refs hmarr/mongoengine#529
2012-07-11 16:38:15 +01:00
Ross Lawley
0b6b0368c5 Merge branch 'master' of https://github.com/elasticsales/mongoengine 2012-07-11 16:36:35 +01:00
Ross Lawley
d1d30a9280 Added test and updated changelog
refs hmarr/mongoengine#527
2012-07-11 16:34:28 +01:00
Ross Lawley
420c6f2d1e Merge branch 'patch-10' of https://github.com/wpjunior/mongoengine 2012-07-11 16:33:16 +01:00
Ross Lawley
34f06c4971 Updated changelog / AUTHORS
refs hmarr/mongoengine#524
2012-07-11 16:27:43 +01:00
Ross Lawley
9cc4bbd49d Merge branch 'patch-1' of https://github.com/daevaorn/mongoengine 2012-07-11 16:26:50 +01:00
Ross Lawley
f66b312869 Updated api docs
fixes hmarr/mongoengine#526
2012-07-11 16:25:40 +01:00
Ross Lawley
2405ba8708 Updated Changelog / AUTHORS
refs hmarr/mongoengine#531
2012-07-11 16:11:13 +01:00
Ross Lawley
a91b6bff8b Merge branch 'master' of https://github.com/agonzalezro/mongoengine 2012-07-11 16:09:33 +01:00
Ross Lawley
450dc11a68 Unicode fixes
refs hmarr/mongoengine#533 MongoEngine/mongoengine#32
2012-07-11 16:01:24 +01:00
Ross Lawley
1ce2f84ce5 Updated docs regarding fields
refs hmarr/mongoengine#535
2012-07-11 15:56:34 +01:00
Ross Lawley
f55b241cfa Trying to bump travis 2012-07-11 15:45:31 +01:00
Ross Lawley
34d08ce8ef Only dereference fields than need it
Fixes MongoEngine/mongoengine#31
2012-07-11 15:23:27 +01:00
Ross Lawley
4f5aa8c43b Fixed config / added test 2012-07-11 14:29:35 +01:00
Ross Lawley
27b375060d Updated changelog / AUTHORS
refs MongoEngine/mongoengine#32
2012-07-11 14:25:38 +01:00
Ross Lawley
cbfdc401f7 Merge pull request #32 from jaimeirurzun/master
Fix _transform_update to accept unicode fields

Thanks jaimeirurzun
2012-07-11 06:24:29 -07:00
Ross Lawley
b58bf3e0ce Added support for addToSet and each
fixes MongoEngine/mongoengine#33
2012-07-11 14:22:50 +01:00
Jaime Irurzun
1fff7e9aca Fix _transform_update to accept unicode fields 2012-07-10 10:40:14 +01:00
Álex González
494b981b13 Default value for direction 2012-07-02 10:05:25 +02:00
Álex González
dd93995bd0 Forced cast to list 2012-07-02 10:01:22 +02:00
Thomas Steinacher
b3bb4add9c Fix error dict with nested validation. 2012-06-27 13:46:06 -07:00
Wilson Júnior
d305e71c27 Fixes for __ne operator in IntField and FloatField 2012-06-25 15:53:42 -03:00
Alexander Koshelev
0d92baa670 Exclude tests from installation 2012-06-24 03:08:49 +04:00
Ross Lawley
7a1b110f62 Added Tristan Escalada to authors
refs #hmarr/mongoengine#520
2012-06-23 22:24:09 +01:00
Ross Lawley
db8df057ce Merge pull request #520 from tescalada/patch-1
documentation typo: inheritence
2012-06-23 14:23:07 -07:00
Ross Lawley
5d8ffded40 Fixed issue with embedded_docs and db_fields
Bumped version also
refs: hmarr/mongoengine#523
2012-06-23 22:19:02 +01:00
Ross Lawley
07f3e5356d Updated changelog / AUTHORS
refs: hmarr/mongoengine#522
2012-06-23 21:46:31 +01:00
Ross Lawley
1ece62f960 Merge branch 'unicode-fix' of https://github.com/aparajita/mongoengine 2012-06-23 21:43:09 +01:00
Ross Lawley
056c604dc3 Fixes __repr__ modifying the cursor
Fixes MongoEngine/mongoengine#30
2012-06-22 16:22:27 +01:00
Aparajita Fishman
2d08eec093 Fix conversion of StringField value to unicode, replace outdated (str, unicode) check with unicode 2012-06-21 18:57:14 -07:00
Tristan Escalada
614b590551 documentation typo: inheritence
inheritence corrected to inheritance
only in the documentation, not in the code
2012-06-19 17:08:28 -03:00
Ross Lawley
6d90ce250a Version bump 2012-06-19 17:01:28 +01:00
Ross Lawley
ea31846a19 Fixes scalar lookups for primary_key
fixes hmarr/mongoengine#519
2012-06-19 16:59:18 +01:00
Ross Lawley
e6317776c1 Fixes DBRef handling in _delta
refs: hmarr/mongoengine#518
2012-06-19 16:45:23 +01:00
Ross Lawley
efeaba39a4 Version bump 2012-06-19 14:34:16 +01:00
Ross Lawley
1a97dfd479 Better fix for .save() _delta issue with DbRefs
refs: hmarr/mongoengine#518
2012-06-19 14:05:53 +01:00
Ross Lawley
9fecf2b303 Fixed inconsistency handling None values field attrs
fixes hmarr/mongoengine#505
2012-06-19 11:22:12 +01:00
Ross Lawley
3d0d2f48ad Fixed map_field embedded db_field bug
fixes hmarr/mongoengine#512
2012-06-19 10:57:43 +01:00
Ross Lawley
581605e0e2 Added test case for _delta
refs: hmarr/mongoengine#518
2012-06-19 10:08:56 +01:00
Ross Lawley
45d3a7f6ff Updated Changelog 2012-06-19 09:49:55 +01:00
Ross Lawley
7ca2ea0766 Fixes .save _delta issue with DBRefs
Fixes hmarr/mongoengine#518
2012-06-19 09:49:22 +01:00
Ross Lawley
89220c142b Fixed django test class
refs hmarr/mongoengine#506
2012-06-18 21:18:40 +01:00
Ross Lawley
c73ce3d220 Updated changelog / AUTHORS
refs hmarr/mongoengine#511
2012-06-18 21:13:55 +01:00
Ross Lawley
b0f127af4e Merge branch 'master' of https://github.com/andreyfedoseev/mongoengine 2012-06-18 21:12:52 +01:00
Ross Lawley
766d54795f Merge branch 'master' of https://github.com/MeirKriheli/mongoengine
Conflicts:
	docs/changelog.rst
2012-06-18 21:10:14 +01:00
Ross Lawley
bd41c6eea4 Updated changelog & AUTHORS
refs hmarr/mongoengine#517
2012-06-18 21:04:41 +01:00
Ross Lawley
2435786713 Merge branch 'master' of https://github.com/shaunduncan/mongoengine 2012-06-18 20:55:32 +01:00
Ross Lawley
9e7ea64bd2 Fixed db_field load error
Fixes mongoengine/MongoEngine#45
2012-06-18 20:49:33 +01:00
Ross Lawley
89a6eee6af Fixes cascading saves with filefields
fixes #24 #25
2012-06-18 16:45:14 +01:00
Shaun Duncan
2ec1476e50 Adding test case for self-referencing documents with cascade deletes 2012-06-16 11:05:23 -04:00
Shaun Duncan
2d9b581f34 Adding check if cascade delete is self-referencing. If so, prevent
recursing if there are no objects to evaluate
2012-06-15 15:42:19 -04:00
Harry Marr
5bb63f645b Fix minor typo w/ FloatField 2012-06-08 19:24:10 +02:00
Meir Kriheli
a856c7cc37 Fix formatting of the docstring 2012-06-07 12:36:14 +03:00
Meir Kriheli
26db9d8a9d Documentation for PULL reverse_delete_rule 2012-06-07 12:32:02 +03:00
Meir Kriheli
8060179f6d Implement PULL reverse_delete_rule 2012-06-07 12:16:00 +03:00
Meir Kriheli
77ebd87fed Test PULL reverse_delete_rule 2012-06-07 12:02:19 +03:00
Valentin Gorbunov
e4bc92235d test_save_max_recursion_not_hit_with_file_field added 2012-06-06 15:48:16 +04:00
Ross Lawley
27a4d83ce8 Remove comment - it was wrong 2012-05-29 17:32:41 +01:00
Ross Lawley
ece9b902f8 Setup.py cleanups 2012-05-29 17:32:14 +01:00
Ross Lawley
65a2f8a68b Updated configs 2012-05-29 17:06:03 +01:00
Ross Lawley
9c212306b8 Updated setup / added datetime test 2012-05-29 16:24:25 +01:00
Ross Lawley
1fdc7ce6bb Releasing Version 0.6.10 2012-05-23 08:58:43 +01:00
Andrey Fedoseev
0b22c140c5 Add sensible __eq__ method to EmbeddedDocument 2012-05-22 22:31:59 +06:00
Ross Lawley
944aa45459 Updated changelog 2012-05-21 15:21:45 +01:00
Ross Lawley
c9842ba13a Fix base classes to return
fixes hmarr/mongoengine#507
2012-05-21 15:20:46 +01:00
Ross Lawley
8840680303 Promoted BaseDynamicField to DynamicField
closes mongoengine/mongoengine#22
2012-05-17 21:54:17 +01:00
Ross Lawley
376b9b1316 updated the readme 2012-05-17 21:14:25 +01:00
Ross Lawley
54bb1cb3d9 Updated travis settings and Readme 2012-05-17 16:59:50 +01:00
Ross Lawley
43468b474e Adding travis support 2012-05-17 16:49:13 +01:00
Ross Lawley
28a957c684 Version bump 2012-05-14 12:43:00 +01:00
Ross Lawley
ec5ddbf391 Fixed sparse indexes with inheritance
fixes hmarr/mongoengine#497
2012-05-14 12:06:25 +01:00
Ross Lawley
bab186e195 Reverted document.delete auto gridfs delete 2012-05-14 12:02:07 +01:00
Ross Lawley
bc7e874476 Version 0.6.8 2012-05-09 20:53:18 +01:00
Ross Lawley
97114b5948 Fix for FileField losing ref without default
fixes hmarr/mongoengine#458
2012-05-09 20:50:11 +01:00
Ross Lawley
45e015d71d Added test for keys with spaces 2012-05-09 20:49:34 +01:00
Ross Lawley
0ff6531953 Updated changelog 2012-05-09 15:39:34 +01:00
Ross Lawley
ba298c3cfc Save can be used in assignment 2012-05-09 15:37:07 +01:00
Ross Lawley
0479bea40b Cleaned up GridFS
refs hmarr/mongoengine#465
2012-05-09 15:35:28 +01:00
Ross Lawley
a536097804 Added support for pull operations on nested EmbeddedDocs
fixes mongoengine/mongoengine#16
2012-05-09 14:38:53 +01:00
Ross Lawley
bbefd0fdf9 Added example of bi directional delete rules + test
refs mongoengine/mongoengine#15
2012-05-09 13:54:33 +01:00
Ross Lawley
2aa8b04c21 Implemented Choices for GenericReferenceFields
Refs mongoengine/mongoengine#13
2012-05-09 13:21:53 +01:00
Ross Lawley
aeebdfec51 Implemented Choices for GenericEmbeddedDocuments
Refs mongoengine/mongoengine#13
2012-05-09 12:58:45 +01:00
Ross Lawley
debfcdf498 Updated docs re: required pymongo version
refs #472
2012-05-09 12:19:59 +01:00
Ross Lawley
5c4b33e8e6 Made note stronger re: race condition
Refs #478
2012-05-09 12:13:42 +01:00
Ross Lawley
eb54037b66 Added note that get_or_create contains a race condition
Refs #478
2012-05-09 12:08:00 +01:00
Ross Lawley
f48af8db3b Django 1.4 first session save lost data
fixes #477
2012-05-09 12:00:05 +01:00
Ross Lawley
97c5b957dd Updated docs regarding GridFS
refs #492
2012-05-09 11:39:30 +01:00
Ross Lawley
95e7397803 Merge branch 'master' of https://github.com/deignacio/mongoengine 2012-05-09 11:32:26 +01:00
Ross Lawley
43a989978a Merge branch 'patch-8' of https://github.com/wpjunior/mongoengine 2012-05-09 11:21:54 +01:00
Ross Lawley
27734a7c26 Updated Author / changelog 2012-05-09 11:04:05 +01:00
Anthony Nemitz
dd786d6fc4 fix for #494 2012-05-09 02:54:08 -07:00
Ross Lawley
be1c28fc45 Updated changelog 2012-05-08 18:26:04 +01:00
Ross Lawley
20e41b3523 Make the user model extendable 2012-05-08 18:23:51 +01:00
David Ignacio
e07ecc5cf8 Cleanup referenced GridFS files when a document is deleted
Note that drop_collection is not modified since there is no
guarantee that a GridFS collection holds files for only one
Document class.  Otherwise you could drop files for other fields
or documents accidentally.
2012-05-05 01:33:08 -04:00
Wilson Júnior
3360b72531 Small fixes for GenericReferenceField 2012-05-04 14:58:06 -03:00
Ross Lawley
233b13d670 Version bump 2012-05-01 12:03:33 +01:00
Ross Lawley
5bcbb4fdaa Properly fixed indexing on _id for covered indexes
refs  #4
2012-05-01 12:02:45 +01:00
Ross Lawley
dbe2f5f2b8 Updated setup.py 2012-05-01 11:48:57 +01:00
Ross Lawley
ca8b58d66d Fixed indexing on _id for covered indexes
fixes #4
2012-05-01 11:27:37 +01:00
Ross Lawley
f80f0b416f Updated changelog 2012-05-01 11:04:01 +01:00
Ross Lawley
d7765511ee Invalid DB Data now raises an InvalidDocumentError
fixes #2
2012-05-01 11:03:23 +01:00
Ross Lawley
0240a09056 Cleaned up ValidationError Refs #459 2012-05-01 10:14:16 +01:00
Ross Lawley
ab15c4eec9 Merge branch 'master' of https://github.com/adamreeve/mongoengine into 459 2012-05-01 09:42:25 +01:00
Ross Lawley
4ce1ba81a6 Merge branch 'dev-disable-indexing' of https://github.com/colinhowe/mongoengine
Conflicts:
	mongoengine/queryset.py
2012-05-01 09:37:01 +01:00
Ross Lawley
530440b333 Fixed replicaset_connection test 2012-05-01 09:14:38 +01:00
Ross Lawley
b80fda36af Ensure session save is safe 2012-04-27 14:57:07 +01:00
Ross Lawley
42d24263ef Updated changelog 2012-04-27 10:39:35 +01:00
Ross Lawley
1e2797e7ce Merge branch 'master' of github.com:MongoEngine/mongoengine 2012-04-27 10:34:31 +01:00
Ross Lawley
f7075766fc Merge pull request #11 from gregbanks/insert_kwargs
add parameters to QuerySet.insert to allow control of lower level behavior
2012-04-27 02:34:03 -07:00
Ross Lawley
5647ca70bb Fix variable name bug 2012-04-27 09:52:57 +01:00
Ross Lawley
2b8aa6bafc Fixes read_preference
Fixes mongoengine/mongoengine#10
2012-04-27 09:15:05 +01:00
Greg Banks
410443471c TopLevelDocumentMetaClass sets _meta['index_opts'] not _meta['index_options'] 2012-04-26 14:03:30 -07:00
Greg Banks
0bb9781b91 add "safe" and "write_options" parameters to QuerySet.insert similar to Document.save 2012-04-26 13:56:52 -07:00
Ross Lawley
2769d6d7ca Updated Authors / Changelog 2012-04-25 12:43:17 +01:00
Ross Lawley
120b9433c2 Merge branch 'master' of https://github.com/swashbuckler/mongoengine 2012-04-25 12:40:53 +01:00
Ross Lawley
605092bd88 Updated change log / AUTHORS
Thanks Greg Banks
2012-04-25 12:36:37 +01:00
Ross Lawley
a4a8c94374 Merge branch 'master' of https://github.com/gregbanks/mongoengine 2012-04-25 12:31:11 +01:00
Ross Lawley
0e93f6c0db Updated changelog 2012-04-25 12:26:31 +01:00
Ross Lawley
aa2add39ad Version bump 2012-04-25 12:24:08 +01:00
Ross Lawley
a928047147 Fixing sessions for django 1.3 and django 1.4 2012-04-24 21:00:30 +01:00
Jona Andersen
c474ca0f13 Allow File-like objects to be stored.
No longer demands FileField be an actual instance of file, but instead
checks whether object has a 'read' attribute. Fixes read() on
GridFSProxy to return an empty string on read failure, or None if file
does not exist.
2012-04-22 13:49:18 +02:00
Ross Lawley
88dc64653e Fix Django 1.3 auth 2012-04-18 16:41:09 +01:00
Ross Lawley
5f4b70f3a9 Version bump 2012-04-18 10:30:14 +01:00
Ross Lawley
51b429e5b0 Updated changelog 2012-04-18 10:28:54 +01:00
Ross Lawley
360624eb6e Merge branch 'dev' of github.com:hmarr/mongoengine into dev 2012-04-18 10:28:06 +01:00
Ross Lawley
d9d2291837 Merge branch 'master' into dev 2012-04-18 10:27:57 +01:00
Ross Lawley
cbdf816232 Merge branch 'master' of github.com:hmarr/mongoengine 2012-04-18 10:23:02 +01:00
Ross Lawley
2d71eb8a18 Added support back for Django 1.3 as well as 1.4 2012-04-18 10:22:26 +01:00
Ross Lawley
64d2532ce9 Merge pull request #484 from dcrosta/replica-set-connection
refactor get_connection

Thanks @dcrosta
2012-04-18 00:51:30 -07:00
Dan Crosta
0376910f33 refactor get_connection
In the previous version, the requested ReadPreference was ignored in the
case that the user specified a MongoDB URI. This rearranges the code to
ensure that only those values which we explicitly parse out of the URI
override values set as keyword arguments.

This leaves open the possibility of conflicts between the URI and the
kwargs -- we should consider whether to raise an exception if, e.g.,
username is specified as a kwarg *and* in the URI.
2012-04-17 19:50:22 -04:00
Ross Lawley
6d503119a1 Merge pull request #482 from wpjunior/patch-7
Small fixes for ReferenceField
2012-04-16 05:25:42 -07:00
Wilson Júnior
bfae93e57e small fixes for ReferenceField 2012-04-13 04:56:20 -03:00
Greg Banks
49a66ba81a whoops, don't dereference all references as the first type encountered 2012-04-12 11:42:10 -07:00
Greg Banks
a1d43fecd9 fix for issue 473 2012-04-11 16:37:22 -07:00
Ross Lawley
d0e42a4798 Merge branch 'master' into dev 2012-03-27 01:47:48 +01:00
Ross Lawley
2a34358abc Updated connection refs #474 2012-03-27 01:47:17 +01:00
Ross Lawley
fd2bb8ea45 Merge pull request #474 from samuelclay/patch-1
The port is defaulted in to conn_settings, so discard the port since hos...
2012-03-26 17:17:26 -07:00
Ross Lawley
98e5daa0e0 Added mostlystatic to the AUTHORS 2012-03-27 00:49:34 +01:00
Samuel Clay
ad2e119282 The port is defaulted in to conn_settings, so discard the port since hosts_or_uri must be used. 2012-03-26 16:48:37 -07:00
Ross Lawley
c20c30d8d1 Merge pull request #471 from mostlystatic/master
Simple fix to unknown connection alias error message
2012-03-26 16:47:46 -07:00
mostlystatic
66d215c9c1 Fix for unknown connection alias error message. 2012-03-24 20:01:40 +00:00
Ross Lawley
46e088d379 Merge branch 'master' into dev 2012-03-24 19:07:17 +00:00
Ross Lawley
bbdd15161a 0.6.3 release 2012-03-24 19:06:08 +00:00
Ross Lawley
ea9dc8cfb8 Merge branch 'master' of github.com:hmarr/mongoengine 2012-03-24 19:03:44 +00:00
Ross Lawley
6bd2ccc9bf UPdated authors 2012-03-24 19:03:24 +00:00
Ross Lawley
56327c6b58 Merge pull request #470 from arbaal/master
Patches for Django 1.4 compability
2012-03-24 12:02:44 -07:00
Nils Hasenbanck
712e8a51e4 Merge branch 'master' of github.com:arbaal/mongoengine
Conflicts:
	mongoengine/django/sessions.py

Signed-off-by: Nils Hasenbanck <nils@hasenbanck.de>
2012-03-24 19:49:02 +01:00
Nils Hasenbanck
421f324f9e Fixed the exception when saving a new session
The session was not created for some reason. Now it is.

Signed-off-by: Nils Hasenbanck <nils@hasenbanck.de>
2012-03-24 19:43:01 +01:00
Nils Hasenbanck
8fe4a70299 Fixed the exception when saving a new session
Signed-off-by: Nils Hasenbanck <nils@hasenbanck.de>
2012-03-24 19:24:42 +01:00
Nils Hasenbanck
3af6d0dbfd Replaces deprecated hasher with new django 1.4 hasher
This way we can even use the new hasher configuration
django 1.4 provides.

Signed-off-by: Nils Hasenbanck <nils@hasenbanck.de>
2012-03-24 11:08:00 +01:00
Nils Hasenbanck
e2bef076d3 Fixed the session backend for django 1.4
Signed-off-by: Nils Hasenbanck <nils@hasenbanck.de>
2012-03-24 11:07:37 +01:00
Ross Lawley
1bf9f28f4b Merge branch 'master' into dev 2012-03-22 15:49:57 +00:00
Ross Lawley
f1e7b97a93 Updated changelog 2012-03-22 15:48:54 +00:00
Ross Lawley
8cfe13ad90 Merge branch 'master' into dev 2012-03-22 15:46:32 +00:00
Ross Lawley
0f420abc8e Added test for listfields containing embedded documents
Added Adam to the authors - thanks for the patch
fixes #466
2012-03-22 15:44:22 +00:00
Ross Lawley
3b5b715567 Merge branch 'dev' of https://github.com/aparrish/mongoengine into 466 2012-03-22 15:32:45 +00:00
Adam Parrish
520051af25 preparing values in a ListField won't mangle embedded documents any more 2012-03-21 11:03:49 -07:00
Colin Howe
7e376b40bb Add new meta option to Document: allow_index_creation.
Defaults to True. If set to False then MongoEngine will not ensure indexes exist
2012-03-19 20:27:08 +00:00
Ross Lawley
fd18a48608 Rst fix 2012-03-15 16:36:04 +00:00
Ross Lawley
64860c6287 Fix signals documentation 2012-03-15 16:27:31 +00:00
Ross Lawley
58635b24ba Updated changelog 2012-03-12 10:35:02 +00:00
Ross Lawley
3ec9dfc108 Merge branch 'master' into dev 2012-03-12 10:33:08 +00:00
Ross Lawley
bd1572f11a Fixed upgrade docs and instructions 2012-03-12 10:31:51 +00:00
Adam Reeve
540a0cc59c List all document errors when raising a ValidationError 2012-03-09 22:20:59 +13:00
Adam Reeve
83eb4f6b16 Document the ValidationError class 2012-03-09 22:20:54 +13:00
Ross Lawley
95c58bd793 Merge branch 'master' into dev 2012-03-08 12:40:20 +00:00
Ross Lawley
65591c7727 Version Bump 2012-03-08 12:40:07 +00:00
Ross Lawley
737cbf5f60 Updated docs and added fix for _types and positional operator
Bumped version to 0.6.2
2012-03-08 12:39:25 +00:00
Ross Lawley
4c67cbb4b7 Updated upgrade docs. Reported issues with older pymongo and sharding 2012-03-06 12:31:14 +00:00
Ross Lawley
ed2cc2a60b Adding Jacob Peddicord to Authors 2012-03-05 16:20:37 +00:00
Ross Lawley
859e9b3cc4 Version bump 2012-03-05 16:16:37 +00:00
Ross Lawley
c34e79fad9 Fix replicaset connection 2012-03-05 16:15:06 +00:00
Ross Lawley
82446d641e Updated the spec 2012-03-05 14:28:33 +00:00
Ross Lawley
9451c9f331 Updated Readme - points to readthedocs 2012-03-05 13:14:36 +00:00
Ross Lawley
61411bb259 Doc updates 2012-03-05 12:26:44 +00:00
Ross Lawley
fcdb0eff8f Added for read the docs 2012-03-05 12:21:53 +00:00
Ross Lawley
30d9347272 Updated readme 2012-03-05 12:20:59 +00:00
Ross Lawley
7564bbdee8 Fix docs 2012-03-05 11:43:54 +00:00
Ross Lawley
69251e5000 Updated docs 2012-03-05 11:35:12 +00:00
Ross Lawley
6ecdc7b59d Added FutureWarning for inherited classes not declaring allow_inheritance
Refs #437
2012-03-05 11:25:13 +00:00
Ross Lawley
b7d0d8f0cc Added warning to SortedListField 2012-03-05 11:20:22 +00:00
Ross Lawley
df52ed1162 Merge pull request #449 from kajic/uri-fix
Uri connection fix
2012-03-05 00:23:18 -08:00
Ross Lawley
aa6370dd5d Merge pull request #448 from kajic/delta-fix
Thanks Kajic
2012-03-05 00:23:04 -08:00
Robert Kajic
c272b7901f Fix for bug where changes to a a embedded document field are not recorded if the root document was just created+saved. 2012-03-02 15:35:15 +01:00
Robert Kajic
c61de6540a Don't ignore kwargs for uri style connections 2012-03-02 15:33:54 +01:00
Robert Kajic
3c7bf50089 Make uri style connections use parameters not specified in the uri, as well as other keyword arguments 2012-03-02 15:33:38 +01:00
Ross Lawley
32fc4152a7 Enable covered indexes for simple documents.
Refs #444
2012-03-02 13:42:24 +00:00
Ross Lawley
bdf7187d5c Added a limit to .get 2012-02-29 14:57:24 +00:00
Ross Lawley
1639576203 Bumped version 2012-02-29 12:05:47 +00:00
Ross Lawley
ae20c785ea Updated docs 2012-02-29 12:04:43 +00:00
Ross Lawley
a2eb876f8c No longer always upsert on save
closes #407
2012-02-29 11:39:10 +00:00
Ross Lawley
5a1eaa0a98 Updated new deref test 2012-02-29 11:23:43 +00:00
Ross Lawley
398fd4a548 Merge branch 'cleaned_dev' of https://github.com/Ankhbayar/mongoengine into test 2012-02-29 11:13:48 +00:00
Ross Lawley
44b9fb66e1 Updates must have an operation
Closes #387
2012-02-29 11:04:09 +00:00
Ross Lawley
2afa2171f9 Updated Changelog 2012-02-29 10:32:58 +00:00
Ross Lawley
1d7ea71c0d DeReference is now used in a thread safe manner
No global / module instance is needed
Fixes #399
2012-02-29 10:31:33 +00:00
Ross Lawley
2a391f0f16 Raise an error if trying to perform a join
You can't join across reference fields, so raise an error
if someone tries to.
2012-02-29 10:10:51 +00:00
Ross Lawley
e9b8093dac Updated changelog 2012-02-29 10:09:16 +00:00
Ross Lawley
6a229cfbc5 Updates can now take raw queries 2012-02-24 15:48:32 +00:00
Ross Lawley
3300f409ba Update Authors and changelist 2012-02-24 10:34:51 +00:00
Ross Lawley
4466005363 Merge branch 'dev' of github.com:hmarr/mongoengine into dev 2012-02-24 10:29:06 +00:00
Ross Lawley
296ef5bddf Merge branch 'geo2d' into dev 2012-02-24 10:28:52 +00:00
Ross Lawley
1f2a432e82 Merge pull request #439 from kajic/dev
register_connection bugfix
2012-02-24 02:12:55 -08:00
Robert Kajic
855933ab2a uri_dict will have the 'database' key even if the database wasn't present in the uri. We must check it's value as well. 2012-02-24 00:07:57 +01:00
Ross Lawley
ece8d25187 Added replicaset connection support
Provide replicaSet=NAME in your connection.
fixes #423
2012-02-17 17:09:48 +00:00
Ross Lawley
589a720162 Updated changelog 2012-02-17 17:09:14 +00:00
Ross Lawley
a59b518cf2 Updates to imports for future pymongo 2.2 2012-02-17 11:18:25 +00:00
Ross Lawley
a15352a4f8 Merge branch 'dev' of github.com:hmarr/mongoengine into dev 2012-02-17 09:41:41 +00:00
Ross Lawley
df65f3fc3f base no longer expects meta to have allow_inheritance
Closes #430 #431
2012-02-17 09:41:01 +00:00
Robert Kajic
734986c1b5 Documentation on geospatial indexes and how to create them explicitly 2012-02-16 10:41:47 +01:00
Анхбаяр Лхагвадорж
4a9ed5f2f2 Fix derefcence failed some case. 2012-02-02 18:33:12 +08:00
Ross Lawley
088f229865 Merge pull request #421 from linuxnow/integration/dev
Integration/dev
2012-02-01 04:05:06 -08:00
Pau Aliagas
cb2cb851e2 Fix formatting typo in changelog entry 2012-02-01 12:29:05 +01:00
Robert Kajic
d3962c4f7d Added support for creating a geo2d index by prefixing the field name with a * 2012-01-31 22:31:24 +01:00
Ross Lawley
0301135f96 Added uri style connection handling 2012-01-30 10:24:45 +00:00
Ross Lawley
f59aa922ea Added more .scalar tests 2012-01-27 12:20:47 +00:00
Ross Lawley
f60a49d6f6 Added .scalar to Queryset
More efficient than the previous .values_list implementation Ref #393
Reverted some of the .values_list code thats no longer needed.

Closes #415
2012-01-27 11:45:12 +00:00
Alice Bevan-McGregor
9a190eb00d Added ability to have scalar return values instead of partially-populated Document instances. 2012-01-27 11:45:11 +00:00
Ross Lawley
6bad4bd415 Merge pull request #412 from faulkner/dict-update
Add dict.update() support to BaseDict.

Thx Faulkner :)
2012-01-16 03:32:45 -08:00
Chris Faulkner
50d9b0b796 Add dict.update() support to BaseDict. 2012-01-16 19:13:03 +08:00
Ross Lawley
12f884e3ac Fixes typo in documents - thanks Shalabh
Closes #406
2012-01-16 09:11:00 +00:00
Ross Lawley
02b1aa7355 Added Ashwin Purohit to authors
Refs #410
2012-01-16 09:06:18 +00:00
Ross Lawley
90bfa608dd Merge branch 'master' into dev
Conflicts:
	docs/guide/signals.rst
	python-mongoengine.spec
2012-01-16 09:05:38 +00:00
Ross Lawley
13f38b1c1d Merge pull request #410 from purohit/master
typo fix
2012-01-16 01:04:29 -08:00
Ross Lawley
1afe7240f4 Fixed pagination limit / skip bug
fixes #398
2012-01-16 09:03:12 +00:00
Ashwin Purohit
7a41155178 typo in signals guide 2012-01-14 23:21:43 -08:00
Ross Lawley
39a20ea471 Merge pull request #403 from wpjunior/patch-5
Fixes for None values in QuerySet.values_list
2012-01-06 01:17:27 -08:00
Wilson Júnior
d8855a4a0f fixes for None values in QuerySet.values_list 2012-01-05 13:35:32 -02:00
Ross Lawley
de8da78042 Update docs/changelog.rst 2012-01-03 20:42:24 +00:00
Ross Lawley
318b42dff2 Merge pull request #393 from wpjunior/queryset_select
select method in Queryset
2012-01-03 12:40:49 -08:00
Ross Lawley
0018674b62 Update docs/changelog.rst 2012-01-03 20:37:33 +00:00
Ross Lawley
82913e8d69 Merge pull request #396 from wpjunior/mixin_inheritance
Mixin inheritance
2012-01-03 12:36:45 -08:00
Wilson Júnior
0d867a108d mixin inheritance 2011-12-19 11:31:42 -02:00
Wilson Júnior
5ee4b4a5ac added count/len for ListResult 2011-12-16 11:49:20 -02:00
Wilson Júnior
62219d9648 changed name 2011-12-16 11:07:38 -02:00
Ross Lawley
6d9bfff19c Started work on performance
Added an initial benchmark.py
Much more performant than 0.5.2 but still work todo.
2011-12-16 12:41:47 +00:00
Ross Lawley
7614b92197 Fixes super in BaseDict
Closes #395
2011-12-15 09:16:35 +00:00
Wilson Júnior
7c1afd0031 tests for db_field 2011-12-13 11:56:35 -02:00
Wilson Júnior
ca7b2371fb added support for dereferences 2011-12-13 11:54:19 -02:00
Wilson Júnior
ed5fba6b0f support for embedded fields 2011-12-13 07:46:49 -02:00
Ross Lawley
2b3b3bf652 Prelim PyPy support
Refs: #392
2011-12-12 16:26:10 +00:00
Ross Lawley
11daf706df Added Sharding support
Added shard_key meta, so save() and update() passes shard keys
to the pymongo query.  Also made shard key fields immutable.

Closes #388 and #389
2011-12-12 16:13:19 +00:00
Wilson Júnior
4a269eb2c4 added .select method 2011-12-12 13:39:37 -02:00
Ross Lawley
9b3899476c Allow arbitary kwargs to be passed to pymongo
Fix pymongo 2.1+ check
Closes #390 closes #378
2011-12-12 09:14:50 +00:00
Ross Lawley
febb3d7e3d Updated connection - so handles < pymongo 2.1
Updated docs

Refs #378
2011-12-09 08:39:50 -08:00
Ross Lawley
83e3c5c7d8 Updated connection for pymongo 2.1 support
closes #378
2011-12-09 08:26:39 -08:00
Ross Lawley
3c271845c9 Merge pull request #386 from wpjunior/patch-4
Custom db_alias support for MongoEngine DjangoSession
2011-12-07 07:56:43 -08:00
Wilson Júnior
56c4292164 added custom db_alias support for MongoEngine DjangoSession 2011-12-07 13:36:35 -02:00
Ross Lawley
2531ade3bb Added David to Authors
refs #380
2011-12-07 03:01:56 -08:00
Ross Lawley
3e2f035400 Abstract documents can now declare indexes
fixes #380
2011-12-07 02:15:50 -08:00
Ross Lawley
e7bcb5e366 Updated docs and Authors list re: db_alias 2011-12-07 01:46:11 -08:00
Ross Lawley
112e921ce2 Syntax cleaning 2011-12-07 01:34:36 -08:00
Ross Lawley
216f15602b Fixing test 2011-12-07 01:17:35 -08:00
Ross Lawley
fbe1901e65 Added some tests #384 2011-12-07 01:16:45 -08:00
Ross Lawley
8d2bc444bb db_alias using in model, queryset, reference fields, derefrence. 2011-12-07 01:16:36 -08:00
Ross Lawley
cf4a45da11 Dynamic Documents now support string query lookups 2011-12-06 06:38:25 -08:00
Ross Lawley
be78209f94 Added test showing you can add index for dynamic docs 2011-12-05 04:44:40 -08:00
Ross Lawley
45b5bf73fe Added Jan to the contributors list
Refs #135 #381
2011-12-05 04:17:51 -08:00
Ross Lawley
84f9e44b6c Fixed GridFS documents can now be pickled
Refs #135 #381
2011-12-05 04:16:57 -08:00
Ross Lawley
700bc1b4bb Multiple fields with the same db_field now raises Exception
Closes #329
2011-12-02 08:44:15 -08:00
Ross Lawley
beef2ede25 Merge pull request #313 from linuxnow/integration/master
add .gitignore and spec file to master
2011-12-02 08:15:24 -08:00
Ross Lawley
9bfc838029 Updated Docs and bumped version
Hopefully nearer 0.6
closes #368
2011-12-02 08:14:25 -08:00
Ross Lawley
e9d7353294 Updated with_id to raise Error if used with a filter.
Closes #365
2011-12-02 07:11:06 -08:00
Ross Lawley
a6948771d8 Added ReferencField handling with .distinct()
Closes #356
2011-12-02 06:47:58 -08:00
Ross Lawley
403977cd49 Added test for saving references unnecessarily.
Refs #359
2011-12-02 06:40:57 -08:00
Ross Lawley
153538cef9 Added test for saving false on dynamic documents
Refs #282
Closes #311
2011-12-02 06:34:51 -08:00
Ross Lawley
9f1196e982 Merge branch 'dev' of github.com:hmarr/mongoengine into dev 2011-12-02 06:04:12 -08:00
Ross Lawley
6419a8d09a Fixed False BooleanField marked as unset by _delta()
Closes #282
2011-12-02 06:03:15 -08:00
Ross Lawley
769cee3d64 Merge pull request #379 from wpjunior/patch-3
Small improvements for item_frequencies
2011-12-02 05:19:49 -08:00
Wilson Júnior
fc460b775e Small improvements for item_frequencies 2011-12-02 09:46:51 -02:00
Ross Lawley
ba59e498de Custom __instancecheck__ no longer needed
Would be needed if calling a classmethod in __new__
but as we dont support reverse_delete_rules on embedded
documents there is no longer the need for it.

Refs #227
2011-12-02 02:52:06 -08:00
Ross Lawley
939bd2bb1f Updated Documentation 2011-12-02 02:49:16 -08:00
Ross Lawley
e231f71b4a EmbeddedDocuments dont support Reverse Delete Rules
Now throws an InvalidDocumentError

Refs #227
2011-12-02 02:46:55 -08:00
Ross Lawley
d06c5f036b Cleaned up _transform_query
Refs #354 #376
2011-12-02 00:37:32 -08:00
Ross Lawley
071562d755 Fixed issue with dynamic documents deltas
Closes #377
2011-12-02 00:11:25 -08:00
Ross Lawley
391f659af1 Updated docs re: reverse delete rules
refs #254
2011-12-01 08:16:13 -08:00
Ross Lawley
8a44232bfc Added Reverse Delete Rule support to ListFields
DictFields and MapFields aren't supported and raise an
InvalidDocument Error

Closes #254
2011-12-01 07:57:24 -08:00
Ross Lawley
9188f9bf62 Added custom cascade kwarg options
Allows the user to overwrite any default kwargs

Closes #295
2011-11-30 08:54:33 -08:00
Ross Lawley
0187a0e113 Handle updating and getting None values
Fixes updating a field to None, so it works in a similar
fashion as unsetting it via save()

Updated to handle null data from the database

Fixes #362
2011-11-30 08:12:44 -08:00
Ross Lawley
beacfae400 Removed use of _get_subclasses favouring get_document
_get_subclasses not actually required and causes issues
where Base Classes aren't imported but dont actually
need to be.

Fixes #271
2011-11-30 07:55:33 -08:00
Ross Lawley
fdc385ea33 Allow dynamic data to be deleted
Fixes #374
2011-11-30 03:06:46 -08:00
Ross Lawley
8b97808931 Added docs for elemMatch 2011-11-30 02:30:29 -08:00
Ross Lawley
179c4a10c8 Merge branch 'elemmatch' of https://github.com/wpjunior/mongoengine into elemmatch 2011-11-30 02:26:54 -08:00
Ross Lawley
6cef571bfb Added Reverse option to SortedLists
Thanks Stephen Young for the patch
closes #364
2011-11-30 02:15:47 -08:00
Ross Lawley
fbe8b28b2e Merge branch 'bug/318' into dev
Conflicts:
	AUTHORS
2011-11-29 03:46:11 -08:00
Ross Lawley
a8d91a56bf Fixes circular list references
The depth deduciton for _fields was over zealous
now max_depth is honoured/

Fixes #373
2011-11-29 03:43:49 -08:00
Ross Lawley
8d7291506e Updated Authors 2011-11-29 01:44:23 -08:00
Wilson Júnior
d9005ac2fc added elemMatch support 2011-11-28 14:45:57 -02:00
Ross Lawley
c775c0a80c Circular references with EmbeddedDocumentField fix
Fixes #345
2011-11-28 08:23:28 -08:00
Ross Lawley
700e2cd93d Updated changelog 2011-11-28 08:16:36 -08:00
Ross Lawley
083f00be84 Fixes passed in Constructor data for complexfields
Fixes #355
2011-11-28 08:09:17 -08:00
Ross Lawley
d00859ecfd Updated changelog - DictField fix 2011-11-28 07:07:26 -08:00
Ross Lawley
4e73566c11 Updated changelog - optional cascasde saves 2011-11-28 07:06:56 -08:00
Ross Lawley
208a467b24 Added dictfield check for Int keys
Fixes #371
2011-11-28 07:05:54 -08:00
Ross Lawley
e1bb453f32 Configurable cascading saves
Updated cascading save logic - can now add meta or pass
cascade to save().  Also Cleaned up reset changed fields logic
as well, so less looping

Refs: #370 #349
2011-11-28 06:51:17 -08:00
Ross Lawley
4607b08be5 Making BaseDict / List more robust 2011-11-28 06:35:19 -08:00
Ross Lawley
aa5c776f3d Copy and paste == brainless 2011-11-28 06:21:45 -08:00
Ross Lawley
0075c0a1e8 Gracefully handle when self.observer is absent
After pickles / deepcopying etc..
2011-11-28 05:54:03 -08:00
Ross Lawley
83fff80b0f Cleaned up dereferencing
Dereferencing now respects max_depth, so should be more performant.
Reload is chainable and can be passed a max_depth for dereferencing
Added an Observer for ComplexBaseFields.

Refs #324 #323 #289
Closes #320
2011-11-25 08:36:47 -08:00
Ross Lawley
5e553ffaf7 Added reconnect back into the syntax
forces a disconnect.
2011-11-24 00:59:43 -08:00
Ross Lawley
6d185b7f7a Merge pull request #366 from wpjunior/filemultidb
db_alias support for FileFields
2011-11-22 12:17:35 -08:00
Ross Lawley
e80144e9f2 Added multidb support
No change required to upgrade to multiple databases. Aliases are used
to describe the database and these can be manually registered or fall
through to a default alias using connect.

Made get_connection and get_db first class members of the connection class.
Old style _get_connection and _get_db still supported.

Refs: #84 #87 #93 #215
2011-11-22 08:01:14 -08:00
Wilson Júnior
fa4b820931 added support for db_alias in FileFields 2011-11-22 13:40:01 -02:00
Ross Lawley
63c5a4dd65 Fixes saving document schemas that have changed
Ensures that form defaults which are documents are
automatically marked as changed, so schemas can evolve
without migration issues.

[#360]
2011-11-22 07:34:08 -08:00
Adam Parrish
34646a414c Fixes bug using positional operator to update embedded documents.
append_field wasn't getting reset to True in the loop, so fields wouldn't
get appended to clean_fields after str was encountered

[#354]
2011-11-11 01:10:00 -08:00
Ross Lawley
5aeee9deb2 Added PIL to spec file
[#314]
2011-11-04 01:55:46 -07:00
Ross Lawley
4c1509a62a Updated docs re choices
[#284] [#314]
2011-11-04 01:54:30 -07:00
Ross Lawley
bfdaae944d Merge branch 'dev' of github.com:hmarr/mongoengine into dev 2011-11-04 01:46:45 -07:00
Ross Lawley
4e44198bbd Clean up of choices code and added tests
[#284] [#314]
2011-11-04 01:45:44 -07:00
Ross Lawley
a4e8177b76 Merge branch 'master' of https://github.com/KarimAllah/mongoengine into choices
Conflicts:
	mongoengine/base.py
2011-11-04 01:34:58 -07:00
Ross Lawley
81bf5cb78b Merge pull request #348 from wpjunior/patch-2
fixes for __repr__ documents
2011-11-02 04:54:08 -07:00
Wilson Júnior
a9fc476fb8 fixed errors in repr if unicode string is found 2011-11-02 09:38:26 -02:00
Ross Lawley
26f0c06624 Merge branch 'get_or_create_optizations' of https://github.com/wpjunior/mongoengine into get_or_create_optimizations 2011-11-01 02:21:37 -07:00
Ross Lawley
59bd72a888 Added tests for __repr__ fix 2011-11-01 02:15:31 -07:00
Ross Lawley
7d808b483e Merge branch 'master' of https://github.com/grubberr/mongoengine into slice 2011-11-01 02:14:44 -07:00
Ross Lawley
3ee60affa9 Renamed schema for errors
Now is `to_dict()` as is more explicit
[refs #344 #328]
2011-11-01 01:51:58 -07:00
Ross Lawley
558b8123b5 Merge branch 'validation-schema' of https://github.com/n1k0/mongoengine into validation-schema
Conflicts:
	mongoengine/base.py
	mongoengine/fields.py
2011-11-01 01:45:32 -07:00
Ross Lawley
ecdf2ae5c7 Updated docs and Authors 2011-11-01 01:20:47 -07:00
Ross Lawley
aa9ed614ad Merge branch 'master' of https://github.com/mjtamlyn/mongoengine into transform_update 2011-11-01 01:19:30 -07:00
Sergey Chvalyuk
1acdb880fc fixing #336 2011-10-28 00:23:13 +03:00
Ross Lawley
7cd22aaf83 Removed debug print 2011-10-27 01:18:32 -07:00
Ross Lawley
5eb63cfa30 Updated changelog 2011-10-27 01:14:51 -07:00
Ross Lawley
5dc998ed52 Merge branch 'dev-bulkinsertsignal' of https://github.com/colinhowe/mongoengine into bulk 2011-10-27 01:13:59 -07:00
Wilson Júnior
8074094568 optimizations for get_or_create 2011-10-27 06:02:59 -02:00
Ross Lawley
56d1139d71 Added ImageField Support
Thanks to @wpjunior for the patch
Closes [#298]
2011-10-27 00:58:47 -07:00
Ross Lawley
165cdc8840 Updated changelog 2011-10-27 00:35:34 -07:00
Ross Lawley
c42aef74de Merge pull request #326 from wpjunior/fixes-325
Fixes for #325 issue - Thanks @wpjunior
2011-10-27 00:33:36 -07:00
Ross Lawley
634e1f661f Updated docs / upgrade notes 2011-10-27 00:31:48 -07:00
Nicolas Perriault
a1db437c42 got rid of assert for validation; ValidationError now extends AssertionError for BC purpose 2011-10-25 22:38:43 +02:00
Nicolas Perriault
b8e2bdc99f simpler raising of ValidatioError 2011-10-25 20:04:39 +02:00
Ross Lawley
52d4ea7d78 Merge pull request #316 from wpjunior/fixes-315
Fixes for #315 issue
2011-10-25 03:47:19 -07:00
Nicolas Perriault
7db5335420 fixed URLField.validate() wasn't using BaseField.error() to raise a ValidationError 2011-10-25 10:53:58 +02:00
Nicolas Perriault
62480fe940 added a ValidatorError.schema properties which contains a dict representation of the whole validation error schema 2011-10-24 17:15:34 +02:00
Nicolas Perriault
3d7b30da77 first version of BC validation schema 2011-10-24 01:02:31 +02:00
Wilson Júnior
8e87648d53 added tests for get_or_create 2011-10-19 09:44:49 -02:00
Wilson Júnior
f842c90007 Merge branches 'master' and 'fixes-325' into fixes-325 2011-10-19 06:57:39 -02:00
Wilson Júnior
7f2b686ab5 added drop_collection for test 2011-10-19 06:55:05 -02:00
Wilson Júnior
b09c52fc7e fixes for #325 issue 2011-10-19 06:30:41 -02:00
Pau Aliagas
202d6e414f Update spec file 2011-10-13 20:29:36 +02:00
Wilson Júnior
3d817f145c fixes for #315 issue 2011-10-12 18:28:40 -03:00
Pau Aliagas
181e191fee Add some more files to ignore in .gitignore 2011-10-12 11:00:40 +02:00
Pau Aliagas
79ecf027dd Add dependencies to spec file
Add spec file for rpm-based systems
2011-10-12 11:00:02 +02:00
Ross Lawley
76d771d20f Merge branch 'master' into dev
Conflicts:
	AUTHORS
	docs/changelog.rst
	mongoengine/base.py
2011-10-12 00:35:01 -07:00
Ross Lawley
4d5f602ee7 Bumped the version 2011-10-12 00:31:02 -07:00
Ross Lawley
452bbcc19b Ported fix for Circular Reference bug to Master
Ready for a 0.5.2 release
2011-10-12 00:30:12 -07:00
Ross Lawley
24b8650026 Merge remote branch 'origin/dev' into dev 2011-10-12 00:18:37 -07:00
Ross Lawley
269e6e29d6 Updated Authors 2011-10-12 00:18:12 -07:00
Ross Lawley
c4b0002ddb Fixed typo 2011-10-11 14:59:58 +02:00
Ross Lawley
53598781b8 Facepalm - mutable default argument in method.. 2011-10-11 12:44:41 +02:00
Ross Lawley
0624cdd6e4 Fixes collection creation post drop_collection
Thanks to Julien Rebetez for the original patch
closes [#285]
2011-10-11 02:26:33 -07:00
Ross Lawley
5fb9d61d28 Merge remote branch 'origin/dev' into dev 2011-10-11 00:15:23 -07:00
Ross Lawley
7b1860d17b Fixes tree based circular references
Thanks to jpfarias for the fix.
Also normalised the other circular checks.
2011-10-10 09:16:32 -07:00
Ross Lawley
8797565606 UPdated changelog 2011-10-08 08:13:53 -07:00
Ross Lawley
3d97c41fe9 Merge pull request #305 from linuxnow/integration/error_msgs
Add field name to validation exception messages
2011-10-08 08:13:40 -07:00
Ross Lawley
5edfeb2e29 Merge pull request #303 from linuxnow/integration/uuid
Add UUIDField
2011-10-08 08:08:45 -07:00
Ross Lawley
268908b3b2 Improvements to .get() efficiency
Closes #307 and #290
2011-10-08 08:06:23 -07:00
Ross Lawley
fb70b47acb Merge branch 'master' into dev
Conflicts:
	docs/changelog.rst
2011-10-08 07:33:52 -07:00
Marc Tamlyn
219d316b49 Fix iteration on querysets.
If iteration of a queryset was interrupted (by a break, or a caught
error), the next iterator would start from the second element as the
cursor had already moved to the first. This is fixed by adding a rewind
into the __iter__ method.
2011-10-05 13:26:57 +01:00
Pau Aliagas
3aa2233b5d Add field name to exception messages 2011-10-04 18:35:32 +02:00
Pau Aliagas
d59862ae6e Merge remote-tracking branch 'upstream/dev' into integration/uuid 2011-10-04 16:02:58 +02:00
Pau Aliagas
0a03f9a31a Add unit tests for UUIDField 2011-10-04 15:59:56 +02:00
Ross Lawley
dca135190a Fixed changelog 2011-10-04 04:28:30 -07:00
Ross Lawley
aedcf3dc81 Merge branch 'listfield' into dev
Conflicts:
	docs/changelog.rst
2011-10-04 04:28:04 -07:00
Ross Lawley
6961a9494f Updates to ComplexFields
Required now means they cannot be empty [#302]
2011-10-04 04:26:56 -07:00
Ross Lawley
6d70ef1a08 Updated changelog [#304] 2011-10-04 03:18:39 -07:00
Ross Lawley
e1fc15875d Merge remote branch 'origin/dev' into dev 2011-10-04 03:10:10 -07:00
Ross Lawley
94ae1388b1 Updated .gitignore 2011-10-04 02:59:00 -07:00
Ross Lawley
17728d4e74 Added tests for empty lists 2011-10-04 02:57:50 -07:00
Ross Lawley
417aa743ca Merge pull request #304 from linuxnow/integration/specfile
Add spec file for rpm-based distributions
2011-10-04 02:27:05 -07:00
Pau Aliagas
2f26f7a827 Add dependencies to spec file
Add spec file for rpm-based systems
2011-10-04 10:33:26 +02:00
Pau Aliagas
09f9c59b3d Add some more files to ignore in .gitignore 2011-10-04 10:24:44 +02:00
Pau Aliagas
bec6805296 Add UUIDField 2011-10-04 10:20:41 +02:00
Pau Aliagas
d99c7c20cc Don't allow empty lists when they are required
When using ListField, an empty list is added as the default value.
But when you mark this field as required, you expect it not to be empty,
so this patch makes sure that this is duly checked.
2011-10-04 10:12:21 +02:00
Ross Lawley
60b6ad3fcf Added test for listfield fix
Added Pau Aliagas to authors
[closes #299]
2011-10-03 05:30:23 -07:00
Pau Aliagas
9b4d0f6450 Make sure that ListFields are not strings 2011-10-03 05:29:24 -07:00
Ross Lawley
1a2c74391c Added grubberr to AUTHORS
[Refs #296]
2011-10-03 05:18:25 -07:00
Sergey Chvalyuk
08288e591c small optimizing fix 2011-10-03 05:16:17 -07:00
Ross Lawley
823cf421fa Fixes to circular references.
Removes infinite looping
refs #294
2011-09-29 14:07:30 -07:00
Ross Lawley
3799f27734 Merge branch 'master' of https://github.com/kuno/mongoengine into kuno 2011-09-28 01:51:06 -07:00
Ross Lawley
a7edd8602c Added support for expando style dynamic documents.
Added two new classes: DynamicDocument and DynamicEmbeddedDocument
for handling expando style setting of attributes.

[closes #112]
2011-09-28 01:39:39 -07:00
Karim Allah
c081aca794 Fixing dereferencing when the dereferenced-document wasn't found. 2011-09-25 18:58:40 +02:00
kuno
2ca6648227 fixed indentation error in signal docs 2011-09-20 21:30:20 +08:00
Ross Lawley
1af54f93f5 Merge pull request #287 from wpjunior/fixitem_frequencies
Fix item_frequencies
2011-09-20 06:04:30 -07:00
Wilson Júnior
a9cacd2e06 fixed embedded null item_frequencies 2011-09-20 08:56:30 -03:00
Ross Lawley
f7fbb3d2f6 Relaxed field name checking on embedded documents 2011-09-20 03:45:11 -07:00
Karim Allah
adb7bbeea0 Being compatible with non-django style chioces 2011-09-18 19:48:33 +02:00
Colin Howe
b91db87ae0 Pre and post bulk-insert signals 2011-09-09 19:17:40 +01:00
45 changed files with 5812 additions and 983 deletions

8
.gitignore vendored
View File

@@ -1,7 +1,8 @@
.*
!.gitignore
*.pyc
.*.swp
*~
*.py[co]
.*.sw[po]
*.egg
docs/.build
docs/_build
@@ -12,4 +13,5 @@ env/
.settings
.project
.pydevproject
tests/bugfix.py
tests/test_bugfix.py
htmlcov/

12
.travis.yml Normal file
View File

@@ -0,0 +1,12 @@
# http://travis-ci.org/#!/MongoEngine/mongoengine
language: python
python:
- 2.6
- 2.7
install:
- sudo apt-get install zlib1g zlib1g-dev
- sudo ln -s /usr/lib/i386-linux-gnu/libz.so /usr/lib/
- pip install PIL --use-mirrors ; true
- python setup.py install
script:
- python setup.py test

49
AUTHORS
View File

@@ -1,11 +1,11 @@
The PRIMARY AUTHORS are (and/or have been):
Ross Lawley <ross.lawley@gmail.com>
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>
Ross Lawley <ross.lawley@gmail.com>
Wilson Júnior <wilsonpjunior@gmail.com>
Dan Crosta https://github.com/dcrosta
@@ -67,3 +67,50 @@ that much better:
* Gareth Lloyd
* Albert Choi
* John Arnfield
* grubberr
* Paul Aliagas
* Paul Cunnane
* Julien Rebetez
* Marc Tamlyn
* Karim Allah
* Adam Parrish
* jpfarias
* jonrscott
* Alice Zoë Bevan-McGregor
* Stephen Young
* tkloc
* aid
* yamaneko1212
* dave mankoff
* Alexander G. Morano
* jwilder
* Joe Shaw
* Adam Flynn
* Ankhbayar
* Jan Schrewe
* David Koblas
* Crittercism
* Alvin Liang
* andrewmlevy
* Chris Faulkner
* Ashwin Purohit
* Shalabh Aggarwal
* Chris Williams
* Robert Kajic
* Jacob Peddicord
* Nils Hasenbanck
* mostlystatic
* Greg Banks
* swashbuckler
* Adam Reeve
* Anthony Nemitz
* deignacio
* shaunduncan
* Meir Kriheli
* Andrey Fedoseev
* aparajita
* Tristan Escalada
* Alexander Koshelev
* Jaime Irurzun
* Alexandre González
* Thomas Steinacher

View File

@@ -3,25 +3,29 @@ MongoEngine
===========
:Info: MongoEngine is an ORM-like layer on top of PyMongo.
:Author: Harry Marr (http://github.com/hmarr)
:Maintainer: Ross Lawley (http://github.com/rozza)
.. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master
:target: http://travis-ci.org/MongoEngine/mongoengine
About
=====
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
Documentation available at http://hmarr.com/mongoengine/ - there is currently
a `tutorial <http://hmarr.com/mongoengine/tutorial.html>`_, a `user guide
<http://hmarr.com/mongoengine/userguide.html>`_ and an `API reference
<http://hmarr.com/mongoengine/apireference.html>`_.
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
Documentation available at http://mongoengine-odm.rtfd.org - there is currently
a `tutorial <http://readthedocs.org/docs/mongoengine-odm/en/latest/tutorial.html>`_, a `user guide
<http://readthedocs.org/docs/mongoengine-odm/en/latest/userguide.html>`_ and an `API reference
<http://readthedocs.org/docs/mongoengine-odm/en/latest/apireference.html>`_.
Installation
============
If you have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
you can use ``easy_install -U mongoengine``. Otherwise, you can download the
source from `GitHub <http://github.com/hmarr/mongoengine>`_ and run ``python
source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and run ``python
setup.py install``.
Dependencies
============
- pymongo 1.1+
- pymongo 2.1.1+
- sphinx (optional - for documentation generation)
Examples
@@ -84,14 +88,15 @@ the standard port, and run ``python setup.py test``.
Community
=========
- `MongoEngine Users mailing list
- `MongoEngine Users mailing list
<http://groups.google.com/group/mongoengine-users>`_
- `MongoEngine Developers mailing list
- `MongoEngine Developers mailing list
<http://groups.google.com/group/mongoengine-dev>`_
- `#mongoengine IRC channel <irc://irc.freenode.net/mongoengine>`_
Contributing
============
The source is available on `GitHub <http://github.com/hmarr/mongoengine>`_ - to
The source is available on `GitHub <http://github.com/MongoEngine/mongoengine>`_ - to
contribute to the project, fork it on GitHub and send a pull request, all
contributions and suggestions are welcome!

182
benchmark.py Normal file
View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python
import timeit
def cprofile_main():
from pymongo import Connection
connection = Connection()
connection.drop_database('timeit_test')
connection.disconnect()
from mongoengine import Document, DictField, connect
connect("timeit_test")
class Noddy(Document):
fields = DictField()
for i in xrange(1):
noddy = Noddy()
for j in range(20):
noddy.fields["key" + str(j)] = "value " + str(j)
noddy.save()
def main():
"""
0.4 Performance Figures ...
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
1.1141769886
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
2.37724113464
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
1.92479610443
0.5.X
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
1.10552310944
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
16.5169169903
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
14.9446101189
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
14.912801981
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, force=True
14.9617750645
Performance
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
1.10072994232
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
5.27341103554
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
4.49365401268
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
4.43459296227
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, force=True
4.40114378929
"""
setup = """
from pymongo import Connection
connection = Connection()
connection.drop_database('timeit_test')
"""
stmt = """
from pymongo import Connection
connection = Connection()
db = connection.timeit_test
noddy = db.noddy
for i in xrange(10000):
example = {'fields': {}}
for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j)
noddy.insert(example)
myNoddys = noddy.find()
[n for n in myNoddys] # iterate
"""
print "-" * 100
print """Creating 10000 dictionaries - Pymongo"""
t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1)
setup = """
from pymongo import Connection
connection = Connection()
connection.drop_database('timeit_test')
connection.disconnect()
from mongoengine import Document, DictField, connect
connect("timeit_test")
class Noddy(Document):
fields = DictField()
"""
stmt = """
for i in xrange(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save()
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print "-" * 100
print """Creating 10000 dictionaries - MongoEngine"""
t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1)
stmt = """
for i in xrange(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(safe=False, validate=False)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print "-" * 100
print """Creating 10000 dictionaries - MongoEngine, safe=False, validate=False"""
t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1)
stmt = """
for i in xrange(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(safe=False, validate=False, cascade=False)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print "-" * 100
print """Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False"""
t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1)
stmt = """
for i in xrange(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(force_insert=True, safe=False, validate=False, cascade=False)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print "-" * 100
print """Creating 10000 dictionaries - MongoEngine, force=True"""
t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1)
if __name__ == "__main__":
main()

View File

@@ -6,6 +6,7 @@ Connecting
==========
.. autofunction:: mongoengine.connect
.. autofunction:: mongoengine.register_connection
Documents
=========
@@ -21,9 +22,18 @@ Documents
.. autoclass:: mongoengine.EmbeddedDocument
:members:
.. autoclass:: mongoengine.DynamicDocument
:members:
.. autoclass:: mongoengine.DynamicEmbeddedDocument
:members:
.. autoclass:: mongoengine.document.MapReduceDocument
:members:
.. autoclass:: mongoengine.ValidationError
:members:
Querying
========
@@ -37,25 +47,28 @@ Querying
Fields
======
.. autoclass:: mongoengine.StringField
.. autoclass:: mongoengine.URLField
.. autoclass:: mongoengine.EmailField
.. autoclass:: mongoengine.IntField
.. autoclass:: mongoengine.FloatField
.. autoclass:: mongoengine.DecimalField
.. autoclass:: mongoengine.DateTimeField
.. autoclass:: mongoengine.BinaryField
.. autoclass:: mongoengine.BooleanField
.. autoclass:: mongoengine.ComplexDateTimeField
.. autoclass:: mongoengine.ListField
.. autoclass:: mongoengine.SortedListField
.. autoclass:: mongoengine.DateTimeField
.. autoclass:: mongoengine.DecimalField
.. autoclass:: mongoengine.DictField
.. autoclass:: mongoengine.DynamicField
.. autoclass:: mongoengine.EmailField
.. autoclass:: mongoengine.EmbeddedDocumentField
.. autoclass:: mongoengine.FileField
.. autoclass:: mongoengine.FloatField
.. autoclass:: mongoengine.GenericEmbeddedDocumentField
.. autoclass:: mongoengine.GenericReferenceField
.. autoclass:: mongoengine.GeoPointField
.. autoclass:: mongoengine.ImageField
.. autoclass:: mongoengine.IntField
.. autoclass:: mongoengine.ListField
.. autoclass:: mongoengine.MapField
.. autoclass:: mongoengine.ObjectIdField
.. autoclass:: mongoengine.ReferenceField
.. autoclass:: mongoengine.GenericReferenceField
.. autoclass:: mongoengine.EmbeddedDocumentField
.. autoclass:: mongoengine.GenericEmbeddedDocumentField
.. autoclass:: mongoengine.BooleanField
.. autoclass:: mongoengine.FileField
.. autoclass:: mongoengine.BinaryField
.. autoclass:: mongoengine.GeoPointField
.. autoclass:: mongoengine.SequenceField
.. autoclass:: mongoengine.SortedListField
.. autoclass:: mongoengine.StringField
.. autoclass:: mongoengine.URLField
.. autoclass:: mongoengine.UUIDField

View File

@@ -2,10 +2,192 @@
Changelog
=========
Changes in 0.6.16
=================
- Fixed issue where db_alias wasn't inherited
Changes in 0.6.15
=================
- Updated validation error messages
- Added support for null / zero / false values in item_frequencies
- Fixed cascade save edge case
- Fixed geo index creation through reference fields
- Added support for args / kwargs when using @queryset_manager
- Deref list custom id fix
Changes in 0.6.14
=================
- Fixed error dict with nested validation
- Fixed Int/Float fields and not equals None
- Exclude tests from installation
- Allow tuples for index meta
- Fixed use of str in instance checks
- Fixed unicode support in transform update
- Added support for add_to_set and each
Changes in 0.6.13
================
- Fixed EmbeddedDocument db_field validation issue
- Fixed StringField unicode issue
- Fixes __repr__ modifying the cursor
Changes in 0.6.12
=================
- Fixes scalar lookups for primary_key
- Fixes error with _delta handling DBRefs
Changes in 0.6.11
==================
- Fixed inconsistency handling None values field attrs
- Fixed map_field embedded db_field issue
- Fixed .save() _delta issue with DbRefs
- Fixed Django TestCase
- Added cmp to Embedded Document
- Added PULL reverse_delete_rule
- Fixed CASCADE delete bug
- Fixed db_field data load error
- Fixed recursive save with FileField
Changes in 0.6.10
=================
- Fixed basedict / baselist to return super(..)
- Promoted BaseDynamicField to DynamicField
Changes in 0.6.9
================
- Fixed sparse indexes on inherited docs
- Removed FileField auto deletion, needs more work maybe 0.7
Changes in 0.6.8
================
- Fixed FileField losing reference when no default set
- Removed possible race condition from FileField (grid_file)
- Added assignment to save, can now do: b = MyDoc(**kwargs).save()
- Added support for pull operations on nested EmbeddedDocuments
- Added support for choices with GenericReferenceFields
- Added support for choices with GenericEmbeddedDocumentFields
- Fixed Django 1.4 sessions first save data loss
- FileField now automatically delete files on .delete()
- Fix for GenericReference to_mongo method
- Fixed connection regression
- Updated Django User document, now allows inheritance
Changes in 0.6.7
================
- Fixed indexing on '_id' or 'pk' or 'id'
- Invalid data from the DB now raises a InvalidDocumentError
- Cleaned up the Validation Error - docs and code
- Added meta `auto_create_index` so you can disable index creation
- Added write concern options to inserts
- Fixed typo in meta for index options
- Bug fix Read preference now passed correctly
- Added support for File like objects for GridFS
- Fix for #473 - Dereferencing abstracts
Changes in 0.6.6
================
- Django 1.4 fixed (finally)
- Added tests for Django
Changes in 0.6.5
================
- More Django updates
Changes in 0.6.4
================
- Refactored connection / fixed replicasetconnection
- Bug fix for unknown connection alias error message
- Sessions support Django 1.3 and Django 1.4
- Minor fix for ReferenceField
Changes in 0.6.3
================
- Updated sessions for Django 1.4
- Bug fix for updates where listfields contain embedded documents
- Bug fix for collection naming and mixins
Changes in 0.6.2
================
- Updated documentation for ReplicaSet connections
- Hack round _types issue with SERVER-5247 - querying other arrays may also cause problems.
Changes in 0.6.1
================
- Fix for replicaSet connections
Changes in 0.6
================
- Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7
- Added support for covered indexes when inheritance is off
- No longer always upsert on save for items with a '_id'
- Error raised if update doesn't have an operation
- DeReferencing is now thread safe
- Errors raised if trying to perform a join in a query
- Updates can now take __raw__ queries
- Added custom 2D index declarations
- Added replicaSet connection support
- Updated deprecated imports from pymongo (safe for pymongo 2.2)
- Added uri support for connections
- Added scalar for efficiently returning partial data values (aliased to values_list)
- Fixed limit skip bug
- Improved Inheritance / Mixin
- Added sharding support
- Added pymongo 2.1 support
- Fixed Abstract documents can now declare indexes
- Added db_alias support to individual documents
- Fixed GridFS documents can now be pickled
- Added Now raises an InvalidDocumentError when declaring multiple fields with the same db_field
- Added InvalidQueryError when calling with_id with a filter
- Added support for DBRefs in distinct()
- Fixed issue saving False booleans
- Fixed issue with dynamic documents deltas
- Added Reverse Delete Rule support to ListFields - MapFields aren't supported
- Added customisable cascade kwarg options
- Fixed Handle None values for non-required fields
- Removed Document._get_subclasses() - no longer required
- Fixed bug requiring subclasses when not actually needed
- Fixed deletion of dynamic data
- Added support for the $elementMatch operator
- Added reverse option to SortedListFields
- Fixed dereferencing - multi directional list dereferencing
- Fixed issue creating indexes with recursive embedded documents
- Fixed recursive lookup in _unique_with_indexes
- Fixed passing ComplexField defaults to constructor for ReferenceFields
- Fixed validation of DictField Int keys
- Added optional cascade saving
- Fixed dereferencing - max_depth now taken into account
- Fixed document mutation saving issue
- Fixed positional operator when replacing embedded documents
- Added Non-Django Style choices back (you can have either)
- Fixed __repr__ of a sliced queryset
- Added recursive validation error of documents / complex fields
- Fixed breaking during queryset iteration
- Added pre and post bulk-insert signals
- Added ImageField - requires PIL
- Fixed Reference Fields can be None in get_or_create / queries
- Fixed accessing pk on an embedded document
- Fixed calling a queryset after drop_collection now recreates the collection
- Add field name to validation exception messages
- Added UUID field
- Improved efficiency of .get()
- Updated ComplexFields so if required they won't accept empty lists / dicts
- Added spec file for rpm-based distributions
- Fixed ListField so it doesnt accept strings
- Added DynamicDocument and EmbeddedDynamicDocument classes for expando schemas
Changes in v0.5.2
=================
- A Robust Circular reference bugfix
Changes in v0.5.1
=================
- Circular reference bugfix
- Fixed simple circular reference bug
Changes in v0.5
===============

View File

@@ -38,7 +38,7 @@ master_doc = 'index'
# General information about the project.
project = u'MongoEngine'
copyright = u'2009-2011, Harry Marr'
copyright = u'2009-2012, MongoEngine Authors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -121,7 +121,7 @@ html_theme_path = ['_themes']
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.

View File

@@ -2,19 +2,21 @@
Using MongoEngine with Django
=============================
.. note :: Updated to support Django 1.4
Connecting
==========
In your **settings.py** file, ignore the standard database settings (unless you
also plan to use the ORM in your project), and instead call
also plan to use the ORM in your project), and instead call
:func:`~mongoengine.connect` somewhere in the settings module.
Authentication
==============
MongoEngine includes a Django authentication backend, which uses MongoDB. The
:class:`~mongoengine.django.auth.User` model is a MongoEngine
:class:`~mongoengine.Document`, but implements most of the methods and
:class:`~mongoengine.django.auth.User` model is a MongoEngine
:class:`~mongoengine.Document`, but implements most of the methods and
attributes that the standard Django :class:`User` model does - so the two are
moderately compatible. Using this backend will allow you to store users in
moderately compatible. Using this backend will allow you to store users in
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**
@@ -24,7 +26,7 @@ file::
'mongoengine.django.auth.MongoEngineBackend',
)
The :mod:`~mongoengine.django.auth` module also contains a
The :mod:`~mongoengine.django.auth` module also contains a
:func:`~mongoengine.django.auth.get_user` helper function, that takes a user's
:attr:`id` and returns a :class:`~mongoengine.django.auth.User` object.
@@ -49,9 +51,9 @@ 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.storage.GridFSStorage`.
storage module is called :class:`~mongoengine.django.storage.GridFSStorage`.
Using it is very similar to using the default FileSystemStorage.::
from mongoengine.django.storage import GridFSStorage
fs = GridFSStorage()

View File

@@ -3,6 +3,7 @@
=====================
Connecting to MongoDB
=====================
To connect to a running instance of :program:`mongod`, use the
:func:`~mongoengine.connect` function. The first argument is the name of the
database to connect to. If the database does not exist, it will be created. If
@@ -18,3 +19,47 @@ provide :attr:`host` and :attr:`port` arguments to
:func:`~mongoengine.connect`::
connect('project1', host='192.168.1.35', port=12345)
Uri style connections are also supported as long as you include the database
name - just supply the uri as the :attr:`host` to
:func:`~mongoengine.connect`::
connect('project1', host='mongodb://localhost/database_name')
ReplicaSets
===========
MongoEngine now supports :func:`~pymongo.replica_set_connection.ReplicaSetConnection`
to use them please use a URI style connection and provide the `replicaSet` name in the
connection kwargs.
Multiple Databases
==================
Multiple database support was added in MongoEngine 0.6. To use multiple
databases you can use :func:`~mongoengine.connect` and provide an `alias` name
for the connection - if no `alias` is provided then "default" is used.
In the background this uses :func:`~mongoengine.register_connection` to
store the data and you can register all aliases up front if required.
Individual documents can also support multiple databases by providing a
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects
to point across databases and collections. Below is an example schema, using
3 different databases to store data::
class User(Document):
name = StringField()
meta = {"db_alias": "user-db"}
class Book(Document):
name = StringField()
meta = {"db_alias": "book-db"}
class AuthorBooks(Document):
author = ReferenceField(User)
book = ReferenceField(Book)
meta = {"db_alias": "users-books-db"}

View File

@@ -24,6 +24,34 @@ objects** as class attributes to the document class::
title = StringField(max_length=200, required=True)
date_modified = DateTimeField(default=datetime.datetime.now)
Dynamic document schemas
========================
One of the benefits of MongoDb is dynamic schemas for a collection, whilst data
should be planned and organised (after all explicit is better than implicit!)
there are scenarios where having dynamic / expando style documents is desirable.
:class:`~mongoengine.DynamicDocument` documents work in the same way as
:class:`~mongoengine.Document` but any data / attributes set to them will also
be saved ::
from mongoengine import *
class Page(DynamicDocument):
title = StringField(max_length=200, required=True)
# Create a new page and add tags
>>> page = Page(title='Using MongoEngine')
>>> page.tags = ['mongodb', 'mongoengine']
>>> page.save()
>>> Page.objects(tags='mongoengine').count()
>>> 1
..note::
There is one caveat on Dynamic Documents: fields cannot start with `_`
Fields
======
By default, fields are not required. To make a field mandatory, set the
@@ -34,28 +62,31 @@ not provided. Default values may optionally be a callable, which will be called
to retrieve the value (such as in the above example). The field types available
are as follows:
* :class:`~mongoengine.StringField`
* :class:`~mongoengine.URLField`
* :class:`~mongoengine.EmailField`
* :class:`~mongoengine.IntField`
* :class:`~mongoengine.FloatField`
* :class:`~mongoengine.DecimalField`
* :class:`~mongoengine.DateTimeField`
* :class:`~mongoengine.BinaryField`
* :class:`~mongoengine.BooleanField`
* :class:`~mongoengine.ComplexDateTimeField`
* :class:`~mongoengine.ListField`
* :class:`~mongoengine.SortedListField`
* :class:`~mongoengine.DateTimeField`
* :class:`~mongoengine.DecimalField`
* :class:`~mongoengine.DictField`
* :class:`~mongoengine.DynamicField`
* :class:`~mongoengine.EmailField`
* :class:`~mongoengine.EmbeddedDocumentField`
* :class:`~mongoengine.FileField`
* :class:`~mongoengine.FloatField`
* :class:`~mongoengine.GenericEmbeddedDocumentField`
* :class:`~mongoengine.GenericReferenceField`
* :class:`~mongoengine.GeoPointField`
* :class:`~mongoengine.ImageField`
* :class:`~mongoengine.IntField`
* :class:`~mongoengine.ListField`
* :class:`~mongoengine.MapField`
* :class:`~mongoengine.ObjectIdField`
* :class:`~mongoengine.ReferenceField`
* :class:`~mongoengine.GenericReferenceField`
* :class:`~mongoengine.EmbeddedDocumentField`
* :class:`~mongoengine.GenericEmbeddedDocumentField`
* :class:`~mongoengine.BooleanField`
* :class:`~mongoengine.FileField`
* :class:`~mongoengine.BinaryField`
* :class:`~mongoengine.GeoPointField`
* :class:`~mongoengine.SequenceField`
* :class:`~mongoengine.SortedListField`
* :class:`~mongoengine.StringField`
* :class:`~mongoengine.URLField`
* :class:`~mongoengine.UUIDField`
Field arguments
---------------
@@ -70,7 +101,7 @@ arguments can be set on all fields:
: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
:class:`~mongoengine.ValidationError` will be raised when the document is
validated.
:attr:`default` (Default: None)
@@ -107,12 +138,33 @@ arguments can be set on all fields:
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.
An iterable (e.g. a list or tuple) of choices to which the value of this
field should be limited.
Can be either be a nested tuples of value (stored in mongo) and a
human readable key ::
SIZE = (('S', 'Small'),
('M', 'Medium'),
('L', 'Large'),
('XL', 'Extra Large'),
('XXL', 'Extra Extra Large'))
class Shirt(Document):
size = StringField(max_length=3, choices=SIZE)
Or a flat iterable just containing values ::
SIZE = ('S', 'M', 'L', 'XL', 'XXL')
class Shirt(Document):
size = StringField(max_length=3, choices=SIZE)
:attr:`help_text` (Default: None)
Optional help text to output with the field - used by form libraries
:attr:`verbose` (Default: None)
:attr:`verbose_name` (Default: None)
Optional human-readable name for the field - used by form libraries
@@ -240,6 +292,10 @@ Its value can take any of the following constants:
:const:`mongoengine.CASCADE`
Any object containing fields that are refererring to the object being deleted
are deleted first.
:const:`mongoengine.PULL`
Removes the reference to the object (using MongoDB's "pull" operation)
from any object's fields of
:class:`~mongoengine.ListField` (:class:`~mongoengine.ReferenceField`).
.. warning::
@@ -382,10 +438,31 @@ If a dictionary is passed then the following options are available:
:attr:`unique` (Default: False)
Whether the index should be sparse.
.. note::
.. warning::
Geospatial indexes will be automatically created for all
:class:`~mongoengine.GeoPointField`\ s
Inheritance adds extra indices.
If don't need inheritance for a document turn inheritance off - see :ref:`document-inheritance`.
Geospatial indexes
---------------------------
Geospatial indexes will be automatically created for all
:class:`~mongoengine.GeoPointField`\ s
It is also possible to explicitly define geospatial indexes. This is
useful if you need to define a geospatial index on a subfield of a
:class:`~mongoengine.DictField` or a custom field that contains a
point. To create a geospatial index you must prefix the field with the
***** sign. ::
class Place(Document):
location = DictField()
meta = {
'indexes': [
'*location.point',
],
}
Ordering
========
@@ -427,8 +504,31 @@ subsequent calls to :meth:`~mongoengine.queryset.QuerySet.order_by`. ::
first_post = BlogPost.objects.order_by("+published_date").first()
assert first_post.title == "Blog Post #1"
Shard keys
==========
If your collection is sharded, then you need to specify the shard key as a tuple,
using the :attr:`shard_key` attribute of :attr:`-mongoengine.Document.meta`.
This ensures that the shard key is sent with the query when calling the
:meth:`~mongoengine.document.Document.save` or
:meth:`~mongoengine.document.Document.update` method on an existing
:class:`-mongoengine.Document` instance::
class LogEntry(Document):
machine = StringField()
app = StringField()
timestamp = DateTimeField()
data = StringField()
meta = {
'shard_key': ('machine', 'timestamp',)
}
.. _document-inheritance:
Document inheritance
====================
To create a specialised type of a :class:`~mongoengine.Document` you have
defined, you may subclass it and add any extra fields or methods you may need.
As this is new class is not a direct subclass of
@@ -440,10 +540,15 @@ convenient and efficient retrieval of related documents::
class Page(Document):
title = StringField(max_length=200, required=True)
meta = {'allow_inheritance': True}
# Also stored in the collection named 'page'
class DatedPage(Page):
date = DateTimeField()
.. note:: From 0.7 onwards you must declare `allow_inheritance` in the document meta.
Working with existing data
--------------------------
To enable correct retrieval of documents involved in this kind of heirarchy,

View File

@@ -35,13 +35,23 @@ already exist, then any changes will be updated atomically. For example::
* ``list_field.pop(0)`` - *sets* the resulting list
* ``del(list_field)`` - *unsets* whole list
To delete a document, call the :meth:`~mongoengine.Document.delete` method.
Note that this will only work if the document exists in the database and has a
valide :attr:`id`.
.. seealso::
:ref:`guide-atomic-updates`
Cascading Saves
---------------
If your document contains :class:`~mongoengine.ReferenceField` or
:class:`~mongoengine.GenericReferenceField` objects, then by default the
:meth:`~mongoengine.Document.save` method will automatically save any changes to
those objects as well. If this is not desired passing :attr:`cascade` as False
to the save method turns this feature off.
Deleting documents
------------------
To delete a document, call the :meth:`~mongoengine.Document.delete` method.
Note that this will only work if the document exists in the database and has a
valid :attr:`id`.
Document IDs
============
Each document in the database has a unique id. This may be accessed through the
@@ -81,5 +91,5 @@ is an alias to :attr:`id`::
.. 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
it.
required, so a :class:`~mongoengine.ValidationError` will be thrown if
you don't provide it.

View File

@@ -65,7 +65,7 @@ Deleting stored files is achieved with the :func:`delete` method::
marmot.photo.delete()
.. note::
.. warning::
The FileField in a Document actually only stores the ID of a file in a
separate GridFS collection. This means that deleting a document

View File

@@ -76,6 +76,7 @@ expressions:
* ``istartswith`` -- string field starts with value (case insensitive)
* ``endswith`` -- string field ends with value
* ``iendswith`` -- string field ends with value (case insensitive)
* ``match`` -- performs an $elemMatch so you can match an entire document within an array
There are a few special operators for performing geographical queries, that
may used with :class:`~mongoengine.GeoPointField`\ s:
@@ -194,22 +195,6 @@ to be created::
>>> a.name == b.name and a.age == b.age
True
Dereferencing results
---------------------
When iterating the results of :class:`~mongoengine.ListField` or
:class:`~mongoengine.DictField` we automatically dereference any
:class:`~pymongo.dbref.DBRef` objects as efficiently as possible, reducing the
number the queries to mongo.
There are times when that efficiency is not enough, documents that have
:class:`~mongoengine.ReferenceField` objects or
:class:`~mongoengine.GenericReferenceField` objects at the top level are
expensive as the number of queries to MongoDB can quickly rise.
To limit the number of queries use
:func:`~mongoengine.queryset.QuerySet.select_related` which converts the
QuerySet to a list and dereferences as efficiently as possible.
Default Document queries
========================
By default, the objects :attr:`~mongoengine.Document.objects` attribute on a
@@ -312,8 +297,16 @@ would be generating "tag-clouds"::
from operator import itemgetter
top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10]
Query efficiency and performance
================================
There are a couple of methods to improve efficiency when querying, reducing the
information returned by the query or efficient dereferencing .
Retrieving a subset of fields
=============================
-----------------------------
Sometimes a subset of fields on a :class:`~mongoengine.Document` is required,
and for efficiency only these should be retrieved from the database. This issue
is especially important for MongoDB, as fields may often be extremely large
@@ -346,6 +339,27 @@ will be given::
If you later need the missing fields, just call
:meth:`~mongoengine.Document.reload` on your document.
Getting related data
--------------------
When iterating the results of :class:`~mongoengine.ListField` or
:class:`~mongoengine.DictField` we automatically dereference any
:class:`~pymongo.dbref.DBRef` objects as efficiently as possible, reducing the
number the queries to mongo.
There are times when that efficiency is not enough, documents that have
:class:`~mongoengine.ReferenceField` objects or
:class:`~mongoengine.GenericReferenceField` objects at the top level are
expensive as the number of queries to MongoDB can quickly rise.
To limit the number of queries use
:func:`~mongoengine.queryset.QuerySet.select_related` which converts the
QuerySet to a list and dereferences as efficiently as possible. By default
:func:`~mongoengine.queryset.QuerySet.select_related` only dereferences any
references to the depth of 1 level. If you have more complicated documents and
want to dereference more of the object at once then increasing the :attr:`max_depth`
will dereference more levels of the document.
Advanced queries
================
Sometimes calling a :class:`~mongoengine.queryset.QuerySet` object with keyword

View File

@@ -5,11 +5,13 @@ Signals
.. versionadded:: 0.5
Signal support is provided by the excellent `blinker`_ library and
will gracefully fall back if it is not available.
.. note::
Signal support is provided by the excellent `blinker`_ library and
will gracefully fall back if it is not available.
The following document signals exist in MongoEngine and are pretty self explaintary:
The following document signals exist in MongoEngine and are pretty self-explanatory:
* `mongoengine.signals.pre_init`
* `mongoengine.signals.post_init`
@@ -17,6 +19,8 @@ The following document signals exist in MongoEngine and are pretty self explaint
* `mongoengine.signals.post_save`
* `mongoengine.signals.pre_delete`
* `mongoengine.signals.post_delete`
* `mongoengine.signals.pre_bulk_insert`
* `mongoengine.signals.post_bulk_insert`
Example usage::
@@ -42,8 +46,8 @@ Example usage::
else:
logging.debug("Updated")
signals.pre_save.connect(Author.pre_save, sender=Author)
signals.post_save.connect(Author.post_save, sender=Author)
signals.pre_save.connect(Author.pre_save, sender=Author)
signals.post_save.connect(Author.post_save, sender=Author)
.. _blinker: http://pypi.python.org/pypi/blinker

View File

@@ -18,6 +18,9 @@ MongoDB. To install it, simply run
:doc:`apireference`
The complete API documentation.
:doc:`upgrade`
How to upgrade MongoEngine.
:doc:`django`
Using MongoEngine and Django
@@ -42,7 +45,8 @@ Also, you can join the developers' `mailing list
Changes
-------
See the :doc:`changelog` for a full list of changes to MongoEngine.
See the :doc:`changelog` for a full list of changes to MongoEngine and
:doc:`upgrade` for upgrade information.
.. toctree::
:hidden:

View File

@@ -167,6 +167,11 @@ To delete all the posts if a user is deleted set the rule::
See :class:`~mongoengine.ReferenceField` for more information.
..note::
MapFields and DictFields currently don't support automatic handling of
deleted references
Adding data to our Tumblelog
============================
Now that we've defined how our documents will be structured, let's start adding

View File

@@ -2,6 +2,24 @@
Upgrading
=========
0.5 to 0.6
==========
Embedded Documents - if you had a `pk` field you will have to rename it from `_id`
to `pk` as pk is no longer a property of Embedded Documents.
Reverse Delete Rules in Embedded Documents, MapFields and DictFields now throw
an InvalidDocument error as they aren't currently supported.
Document._get_subclasses - Is no longer used and the class method has been removed.
Document.objects.with_id - now raises an InvalidQueryError if used with a filter.
FutureWarning - A future warning has been added to all inherited classes that
don't define `allow_inheritance` in their meta.
You may need to update pyMongo to 2.0 for use with Sharding.
0.4 to 0.5
===========
@@ -9,7 +27,7 @@ There have been the following backwards incompatibilities from 0.4 to 0.5. The
main areas of changed are: choices in fields, map_reduce and collection names.
Choice options:
--------------
---------------
Are now expected to be an iterable of tuples, with the first element in each
tuple being the actual value to be stored. The second element is the
@@ -58,7 +76,7 @@ To upgrade use a Mixin class to set meta like so ::
class MyAceDocument(Document, BaseMixin):
pass
MyAceDocument._get_collection_name() == myacedocument
MyAceDocument._get_collection_name() == "myacedocument"
Alternatively, you can rename your collections eg ::

View File

@@ -12,9 +12,7 @@ from signals import *
__all__ = (document.__all__ + fields.__all__ + connection.__all__ +
queryset.__all__ + signals.__all__)
__author__ = 'Harry Marr'
VERSION = (0, 5, 1)
VERSION = (0, 6, 16)
def get_version():

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,166 @@
from pymongo import Connection
import multiprocessing
import threading
__all__ = ['ConnectionError', 'connect']
import pymongo
from pymongo import Connection, ReplicaSetConnection, uri_parser
_connection_defaults = {
'host': 'localhost',
'port': 27017,
}
_connection = {}
_connection_settings = _connection_defaults.copy()
__all__ = ['ConnectionError', 'connect', 'register_connection',
'DEFAULT_CONNECTION_NAME']
_db_name = None
_db_username = None
_db_password = None
_db = {}
DEFAULT_CONNECTION_NAME = 'default'
class ConnectionError(Exception):
pass
def _get_connection(reconnect=False):
"""Handles the connection to the database
_connection_settings = {}
_connections = {}
_dbs = {}
def register_connection(alias, name, host='localhost', port=27017,
is_slave=False, read_preference=False, slaves=None,
username=None, password=None, **kwargs):
"""Add a connection.
:param alias: the name that will be used to refer to this connection
throughout MongoEngine
:param name: the name of the specific database to use
:param host: the host name of the :program:`mongod` instance to connect to
:param port: the port that the :program:`mongod` instance is running on
:param is_slave: whether the connection can act as a slave ** Depreciated pymongo 2.0.1+
:param read_preference: The read preference for the collection ** Added pymongo 2.1
:param slaves: a list of aliases of slave connections; each of these must
be a registered connection that has :attr:`is_slave` set to ``True``
:param username: username to authenticate with
:param password: password to authenticate with
:param kwargs: allow ad-hoc parameters to be passed into the pymongo driver
"""
global _connection
identity = get_identity()
global _connection_settings
conn_settings = {
'name': name,
'host': host,
'port': port,
'is_slave': is_slave,
'slaves': slaves or [],
'username': username,
'password': password,
'read_preference': read_preference
}
# Handle uri style connections
if "://" in host:
uri_dict = uri_parser.parse_uri(host)
if uri_dict.get('database') is None:
raise ConnectionError("If using URI style connection include "\
"database name in string")
conn_settings.update({
'host': host,
'name': uri_dict.get('database'),
'username': uri_dict.get('username'),
'password': uri_dict.get('password'),
'read_preference': read_preference,
})
if "replicaSet" in host:
conn_settings['replicaSet'] = True
conn_settings.update(kwargs)
_connection_settings[alias] = conn_settings
def disconnect(alias=DEFAULT_CONNECTION_NAME):
global _connections
global _dbs
if alias in _connections:
get_connection(alias=alias).disconnect()
del _connections[alias]
if alias in _dbs:
del _dbs[alias]
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
global _connections
# Connect to the database if not already connected
if _connection.get(identity) is None or reconnect:
if reconnect:
disconnect(alias)
if alias not in _connections:
if alias not in _connection_settings:
msg = 'Connection with alias "%s" has not been defined' % alias
if alias == DEFAULT_CONNECTION_NAME:
msg = 'You have not defined a default connection'
raise ConnectionError(msg)
conn_settings = _connection_settings[alias].copy()
if hasattr(pymongo, 'version_tuple'): # Support for 2.1+
conn_settings.pop('name', None)
conn_settings.pop('slaves', None)
conn_settings.pop('is_slave', None)
conn_settings.pop('username', None)
conn_settings.pop('password', None)
else:
# Get all the slave connections
if 'slaves' in conn_settings:
slaves = []
for slave_alias in conn_settings['slaves']:
slaves.append(get_connection(slave_alias))
conn_settings['slaves'] = slaves
conn_settings.pop('read_preference', None)
connection_class = Connection
if 'replicaSet' in conn_settings:
conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
# Discard port since it can't be used on ReplicaSetConnection
conn_settings.pop('port', None)
# Discard replicaSet if not base string
if not isinstance(conn_settings['replicaSet'], basestring):
conn_settings.pop('replicaSet', None)
connection_class = ReplicaSetConnection
try:
_connection[identity] = Connection(**_connection_settings)
_connections[alias] = connection_class(**conn_settings)
except Exception, e:
raise ConnectionError("Cannot connect to the database:\n%s" % e)
return _connection[identity]
raise ConnectionError("Cannot connect to database %s :\n%s" % (alias, e))
return _connections[alias]
def _get_db(reconnect=False):
"""Handles database connections and authentication based on the current
identity
def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
global _dbs
if reconnect:
disconnect(alias)
if alias not in _dbs:
conn = get_connection(alias)
conn_settings = _connection_settings[alias]
_dbs[alias] = conn[conn_settings['name']]
# Authenticate if necessary
if conn_settings['username'] and conn_settings['password']:
_dbs[alias].authenticate(conn_settings['username'],
conn_settings['password'])
return _dbs[alias]
def connect(db, alias=DEFAULT_CONNECTION_NAME, **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.
Multiple databases are supported by using aliases. Provide a separate
`alias` to connect to a different instance of :program:`mongod`.
.. versionchanged:: 0.6 - added multiple database support.
"""
global _db, _connection
identity = get_identity()
# Connect if not already connected
if _connection.get(identity) is None or reconnect:
_connection[identity] = _get_connection(reconnect=reconnect)
global _connections
if alias not in _connections:
register_connection(alias, db, **kwargs)
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[identity] = _connection[identity][_db_name]
if _db_username and _db_password:
_db[identity].authenticate(_db_username, _db_password)
return _db[identity]
def get_identity():
"""Creates an identity key based on the current process and thread
identity.
"""
identity = multiprocessing.current_process()._identity
identity = 0 if not identity else identity[0]
identity = (identity, threading.current_thread().ident)
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, _db
_connection_settings = dict(_connection_defaults, **kwargs)
_db_name = db
_db_username = username
_db_password = password
return _get_db(reconnect=True)
return get_connection(alias)
# Support old naming convention
_get_connection = get_connection
_get_db = get_db

View File

@@ -1,17 +1,15 @@
import operator
from bson import DBRef, SON
import pymongo
from base import BaseDict, BaseList, get_document, TopLevelDocumentMetaclass
from fields import ReferenceField
from connection import _get_db
from base import (BaseDict, BaseList, TopLevelDocumentMetaclass, get_document)
from fields import (ReferenceField, ListField, DictField, MapField)
from connection import get_db
from queryset import QuerySet
from document import Document
class DeReference(object):
def __call__(self, items, max_depth=1, instance=None, name=None, get=False):
def __call__(self, items, max_depth=1, instance=None, name=None):
"""
Cheaply dereferences the items to a set depth.
Also handles the convertion of complex data types.
@@ -45,7 +43,7 @@ class DeReference(object):
self.reference_map = self._find_references(items)
self.object_map = self._fetch_objects(doc_type=doc_type)
return self._attach_objects(items, 0, instance, name, get)
return self._attach_objects(items, 0, instance, name)
def _find_references(self, items, depth=0):
"""
@@ -55,7 +53,7 @@ class DeReference(object):
:param depth: The current depth of recursion
"""
reference_map = {}
if not items:
if not items or depth >= self.max_depth:
return reference_map
# Determine the iterator to use
@@ -65,13 +63,14 @@ class DeReference(object):
iterator = items.iteritems()
# Recursively find dbreferences
depth += 1
for k, item in iterator:
if hasattr(item, '_fields'):
for field_name, field in item._fields.iteritems():
v = item._data.get(field_name, None)
if isinstance(v, (pymongo.dbref.DBRef)):
if isinstance(v, (DBRef)):
reference_map.setdefault(field.document_type, []).append(v.id)
elif isinstance(v, (dict, pymongo.son.SON)) and '_ref' in v:
elif isinstance(v, (dict, SON)) and '_ref' in v:
reference_map.setdefault(get_document(v['_cls']), []).append(v['_ref'].id)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
field_cls = getattr(getattr(field, 'field', None), 'document_type', None)
@@ -80,15 +79,15 @@ class DeReference(object):
if isinstance(field_cls, (Document, TopLevelDocumentMetaclass)):
key = field_cls
reference_map.setdefault(key, []).extend(refs)
elif isinstance(item, (pymongo.dbref.DBRef)):
elif isinstance(item, (DBRef)):
reference_map.setdefault(item.collection, []).append(item.id)
elif isinstance(item, (dict, pymongo.son.SON)) and '_ref' in item:
elif isinstance(item, (dict, SON)) and '_ref' in item:
reference_map.setdefault(get_document(item['_cls']), []).append(item['_ref'].id)
elif isinstance(item, (dict, list, tuple)) and depth <= self.max_depth:
references = self._find_references(item, depth)
elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth:
references = self._find_references(item, depth - 1)
for key, refs in references.iteritems():
reference_map.setdefault(key, []).extend(refs)
depth += 1
return reference_map
def _fetch_objects(self, doc_type=None):
@@ -103,16 +102,26 @@ class DeReference(object):
for key, doc in references.iteritems():
object_map[key] = doc
else: # Generic reference: use the refs data to convert to document
references = _get_db()[col].find({'_id': {'$in': refs}})
for ref in references:
if '_cls' in ref:
doc = get_document(ref['_cls'])._from_son(ref)
else:
if doc_type and not isinstance(doc_type, (ListField, DictField, MapField,) ):
references = doc_type._get_db()[col].find({'_id': {'$in': refs}})
for ref in references:
doc = doc_type._from_son(ref)
object_map[doc.id] = doc
object_map[doc.id] = doc
else:
references = get_db()[col].find({'_id': {'$in': refs}})
for ref in references:
if '_cls' in ref:
doc = get_document(ref["_cls"])._from_son(ref)
elif doc_type is None:
doc = get_document(
''.join(x.capitalize()
for x in col.split('_')))._from_son(ref)
else:
doc = doc_type._from_son(ref)
object_map[doc.id] = doc
return object_map
def _attach_objects(self, items, depth=0, instance=None, name=None, get=False):
def _attach_objects(self, items, depth=0, instance=None, name=None):
"""
Recursively finds all db references to be dereferenced
@@ -122,7 +131,6 @@ class DeReference(object):
:class:`~mongoengine.base.ComplexBaseField`
:param name: The name of the field, used for tracking changes by
:class:`~mongoengine.base.ComplexBaseField`
:param get: A boolean determining if being called by __get__
"""
if not items:
if isinstance(items, (BaseDict, BaseList)):
@@ -130,17 +138,16 @@ class DeReference(object):
if instance:
if isinstance(items, dict):
return BaseDict(items, instance=instance, name=name)
return BaseDict(items, instance, name)
else:
return BaseList(items, instance=instance, name=name)
return BaseList(items, instance, name)
if isinstance(items, (dict, pymongo.son.SON)):
if isinstance(items, (dict, SON)):
if '_ref' in items:
return self.object_map.get(items['_ref'].id, items)
elif '_types' in items and '_cls' in items:
doc = get_document(items['_cls'])._from_son(items)
if not get:
doc._data = self._attach_objects(doc._data, depth, doc, name, get)
doc._data = self._attach_objects(doc._data, depth, doc, name)
return doc
if not hasattr(items, 'items'):
@@ -152,35 +159,34 @@ class DeReference(object):
iterator = items.iteritems()
data = {}
depth += 1
for k, v in iterator:
if is_list:
data.append(v)
else:
data[k] = v
if k in self.object_map:
if k in self.object_map and not is_list:
data[k] = self.object_map[k]
elif hasattr(v, '_fields'):
for field_name, field in v._fields.iteritems():
v = data[k]._data.get(field_name, None)
if isinstance(v, (pymongo.dbref.DBRef)):
if isinstance(v, (DBRef)):
data[k]._data[field_name] = self.object_map.get(v.id, v)
elif isinstance(v, (dict, pymongo.son.SON)) and '_ref' in v:
elif isinstance(v, (dict, SON)) and '_ref' in v:
data[k]._data[field_name] = self.object_map.get(v['_ref'].id, v)
elif isinstance(v, dict) and depth < self.max_depth:
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name, get=get)
elif isinstance(v, (list, tuple)):
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name, get=get)
elif isinstance(v, (dict, list, tuple)) and depth < self.max_depth:
data[k] = self._attach_objects(v, depth, instance=instance, name=name, get=get)
elif isinstance(v, dict) and depth <= self.max_depth:
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name)
elif isinstance(v, (list, tuple)) and depth <= self.max_depth:
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
data[k] = self._attach_objects(v, depth - 1, instance=instance, name=name)
elif hasattr(v, 'id'):
data[k] = self.object_map.get(v.id, v)
if instance and name:
if is_list:
return BaseList(data, instance=instance, name=name)
return BaseDict(data, instance=instance, name=name)
return BaseList(data, instance, name)
return BaseDict(data, instance, name)
depth += 1
return data
dereference = DeReference()

View File

@@ -1,23 +1,39 @@
import datetime
from mongoengine import *
from django.utils.hashcompat import md5_constructor, sha_constructor
from django.utils.encoding import smart_str
from django.contrib.auth.models import AnonymousUser
from django.utils.translation import ugettext_lazy as _
import datetime
try:
from django.contrib.auth.hashers import check_password, make_password
except ImportError:
"""Handle older versions of Django"""
from django.utils.hashcompat import md5_constructor, sha_constructor
def get_hexdigest(algorithm, salt, raw_password):
raw_password, salt = smart_str(raw_password), smart_str(salt)
if algorithm == 'md5':
return md5_constructor(salt + raw_password).hexdigest()
elif algorithm == 'sha1':
return sha_constructor(salt + raw_password).hexdigest()
raise ValueError('Got unknown password algorithm type in password')
def check_password(raw_password, password):
algo, salt, hash = password.split('$')
return hash == get_hexdigest(algo, salt, raw_password)
def make_password(raw_password):
from random import random
algo = 'sha1'
salt = get_hexdigest(algo, str(random()), str(random()))[:5]
hash = get_hexdigest(algo, salt, raw_password)
return '%s$%s$%s' % (algo, salt, hash)
REDIRECT_FIELD_NAME = 'next'
def get_hexdigest(algorithm, salt, raw_password):
raw_password, salt = smart_str(raw_password), smart_str(salt)
if algorithm == 'md5':
return md5_constructor(salt + raw_password).hexdigest()
elif algorithm == 'sha1':
return sha_constructor(salt + raw_password).hexdigest()
raise ValueError('Got unknown password algorithm type in password')
class User(Document):
"""A User document that aims to mirror most of the API specified by Django
at http://docs.djangoproject.com/en/dev/topics/auth/#users
@@ -34,7 +50,7 @@ class User(Document):
email = EmailField(verbose_name=_('e-mail address'))
password = StringField(max_length=128,
verbose_name=_('password'),
help_text=_("Use '[algo]$[salt]$[hexdigest]' or use the <a href=\"password/\">change password form</a>."))
help_text=_("Use '[algo]$[iterations]$[salt]$[hexdigest]' or use the <a href=\"password/\">change password form</a>."))
is_staff = BooleanField(default=False,
verbose_name=_('staff status'),
help_text=_("Designates whether the user can log into this admin site."))
@@ -50,6 +66,7 @@ class User(Document):
verbose_name=_('date joined'))
meta = {
'allow_inheritance': True,
'indexes': [
{'fields': ['username'], 'unique': True}
]
@@ -75,11 +92,7 @@ class User(Document):
assigning to :attr:`~mongoengine.django.auth.User.password` as the
password is hashed before storage.
"""
from random import random
algo = 'sha1'
salt = get_hexdigest(algo, str(random()), str(random()))[:5]
hash = get_hexdigest(algo, salt, raw_password)
self.password = '%s$%s$%s' % (algo, salt, hash)
self.password = make_password(raw_password)
self.save()
return self
@@ -89,8 +102,7 @@ class User(Document):
:attr:`~mongoengine.django.auth.User.password` as the password is
hashed before storage.
"""
algo, salt, hash = self.password.split('$')
return hash == get_hexdigest(algo, salt, raw_password)
return check_password(raw_password, self.password)
@classmethod
def create_user(cls, username, password, email=None):

View File

@@ -1,3 +1,6 @@
from datetime import datetime
from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase, CreateError
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_unicode
@@ -5,16 +8,22 @@ from django.utils.encoding import force_unicode
from mongoengine.document import Document
from mongoengine import fields
from mongoengine.queryset import OperationError
from mongoengine.connection import DEFAULT_CONNECTION_NAME
from datetime import datetime
MONGOENGINE_SESSION_DB_ALIAS = getattr(
settings, 'MONGOENGINE_SESSION_DB_ALIAS',
DEFAULT_CONNECTION_NAME)
class MongoSession(Document):
session_key = fields.StringField(primary_key=True, max_length=40)
session_data = fields.StringField()
expire_date = fields.DateTimeField()
meta = {'collection': 'django_session', 'allow_inheritance': False}
meta = {'collection': 'django_session',
'db_alias': MONGOENGINE_SESSION_DB_ALIAS,
'allow_inheritance': False}
class SessionStore(SessionBase):
@@ -35,7 +44,7 @@ class SessionStore(SessionBase):
def create(self):
while True:
self.session_key = self._get_new_session_key()
self._session_key = self._get_new_session_key()
try:
self.save(must_create=True)
except CreateError:
@@ -45,6 +54,8 @@ class SessionStore(SessionBase):
return
def save(self, must_create=False):
if self.session_key is None:
self._session_key = self._get_new_session_key()
s = MongoSession(session_key=self.session_key)
s.session_data = self.encode(self._get_session(no_load=must_create))
s.expire_date = self.get_expiry_date()

View File

@@ -10,7 +10,7 @@ class MongoTestCase(TestCase):
"""
db_name = 'test_%s' % settings.MONGO_DATABASE_NAME
def __init__(self, methodName='runtest'):
self.db = connect(self.db_name)
self.db = connect(self.db_name).get_db()
super(MongoTestCase, self).__init__(methodName)
def _post_teardown(self):

View File

@@ -1,13 +1,15 @@
from mongoengine import signals
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
ValidationError, BaseDict, BaseList)
from queryset import OperationError
from connection import _get_db
import pymongo
__all__ = ['Document', 'EmbeddedDocument', 'ValidationError',
'OperationError', 'InvalidCollectionError']
from bson.dbref import DBRef
from mongoengine import signals
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
BaseDict, BaseList)
from queryset import OperationError
from connection import get_db, DEFAULT_CONNECTION_NAME
__all__ = ['Document', 'EmbeddedDocument', 'DynamicDocument',
'DynamicEmbeddedDocument', 'OperationError', 'InvalidCollectionError']
class InvalidCollectionError(Exception):
@@ -23,6 +25,10 @@ class EmbeddedDocument(BaseDocument):
__metaclass__ = DocumentMetaclass
def __init__(self, *args, **kwargs):
super(EmbeddedDocument, self).__init__(*args, **kwargs)
self._changed_fields = []
def __delattr__(self, *args, **kwargs):
"""Handle deletions of fields"""
field_name = args[0]
@@ -34,6 +40,10 @@ class EmbeddedDocument(BaseDocument):
else:
super(EmbeddedDocument, self).__delattr__(*args, **kwargs)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self._data == other._data
return False
class Document(BaseDocument):
@@ -70,49 +80,70 @@ class Document(BaseDocument):
names. Index direction may be specified by prefixing the field names with
a **+** or **-** sign.
Automatic index creation can be disabled by specifying
attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to
False then indexes will not be created by MongoEngine. This is useful in
production systems where index creation is performed as part of a deployment
system.
By default, _types will be added to the start of every index (that
doesn't contain a list) if allow_inheritence is True. This can be
doesn't contain a list) if allow_inheritance is True. This can be
disabled by either setting types to False on the specific index or
by setting index_types to False on the meta dictionary for the document.
"""
__metaclass__ = TopLevelDocumentMetaclass
@classmethod
def _get_collection(self):
"""Returns the collection for the document."""
db = _get_db()
collection_name = self._get_collection_name()
@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)
if not hasattr(self, '_collection') or self._collection is None:
@classmethod
def _get_db(cls):
"""Some Model using other db_alias"""
return get_db(cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME ))
@classmethod
def _get_collection(cls):
"""Returns the collection for the document."""
if not hasattr(cls, '_collection') or cls._collection is None:
db = cls._get_db()
collection_name = cls._get_collection_name()
# Create collection as a capped collection if specified
if self._meta['max_size'] or self._meta['max_documents']:
if cls._meta['max_size'] or cls._meta['max_documents']:
# Get max document limit and max byte size from meta
max_size = self._meta['max_size'] or 10000000 # 10MB default
max_documents = self._meta['max_documents']
max_size = cls._meta['max_size'] or 10000000 # 10MB default
max_documents = cls._meta['max_documents']
if collection_name in db.collection_names():
self._collection = db[collection_name]
cls._collection = db[collection_name]
# The collection already exists, check if its capped
# options match the specified capped options
options = self._collection.options()
options = cls._collection.options()
if options.get('max') != max_documents or \
options.get('size') != max_size:
msg = ('Cannot create collection "%s" as a capped '
'collection as it already exists') % self._collection
'collection as it already exists') % cls._collection
raise InvalidCollectionError(msg)
else:
# Create the collection as a capped collection
opts = {'capped': True, 'size': max_size}
if max_documents:
opts['max'] = max_documents
self._collection = db.create_collection(
cls._collection = db.create_collection(
collection_name, **opts
)
else:
self._collection = db[collection_name]
return self._collection
cls._collection = db[collection_name]
return cls._collection
def save(self, safe=True, force_insert=False, validate=True, write_options=None, _refs=None):
def save(self, safe=True, force_insert=False, validate=True, write_options=None,
cascade=None, cascade_kwargs=None, _refs=None):
"""Save the :class:`~mongoengine.Document` to the database. If the
document already exists, it will be updated, otherwise it will be
created.
@@ -128,16 +159,25 @@ class Document(BaseDocument):
:meth:`~pymongo.collection.Collection.save` OR
:meth:`~pymongo.collection.Collection.insert`
which will be used as options for the resultant ``getLastError`` command.
For example, ``save(..., w=2, fsync=True)`` will wait until at least two servers
have recorded the write and will force an fsync on each server being written to.
For example, ``save(..., write_options={w: 2, fsync: True}, ...)`` will
wait until at least two servers have recorded the write and will force an
fsync on each server being written to.
:param cascade: Sets the flag for cascading saves. You can set a default by setting
"cascade" in the document __meta__
:param cascade_kwargs: optional kwargs dictionary to be passed throw to cascading saves
:param _refs: A list of processed references used in cascading saves
.. versionchanged:: 0.5
In existing documents it only saves changed fields using set / unset
Saves are cascaded and any :class:`~pymongo.dbref.DBRef` objects
Saves are cascaded and any :class:`~bson.dbref.DBRef` objects
that have changes are saved as well.
"""
from fields import ReferenceField, GenericReferenceField
.. versionchanged:: 0.6
Cascade saves are optional = defaults to True, if you want fine grain
control then you can turn off using document meta['cascade'] = False
Also you can pass different kwargs to the cascade save using cascade_kwargs
which overwrites the existing kwargs with custom values
"""
signals.pre_save.send(self.__class__, document=self)
if validate:
@@ -148,11 +188,11 @@ class Document(BaseDocument):
doc = self.to_mongo()
created = '_id' in doc
creation_mode = force_insert or not created
created = force_insert or '_id' not in doc
try:
collection = self.__class__.objects._collection
if creation_mode:
if created:
if force_insert:
object_id = collection.insert(doc, safe=safe, **write_options)
else:
@@ -160,21 +200,34 @@ class Document(BaseDocument):
else:
object_id = doc['_id']
updates, removals = self._delta()
if updates:
collection.update({'_id': object_id}, {"$set": updates}, upsert=True, safe=safe, **write_options)
if removals:
collection.update({'_id': object_id}, {"$unset": removals}, upsert=True, safe=safe, **write_options)
# Save any references / generic references
_refs = _refs or []
for name, cls in self._fields.items():
if isinstance(cls, (ReferenceField, GenericReferenceField)):
ref = getattr(self, name)
if ref and str(ref) not in _refs:
_refs.append(str(ref))
ref.save(safe=safe, force_insert=force_insert,
validate=validate, write_options=write_options,
_refs=_refs)
# Need to add shard key to query, or you get an error
select_dict = {'_id': object_id}
shard_key = self.__class__._meta.get('shard_key', tuple())
for k in shard_key:
actual_key = self._db_field_map.get(k, k)
select_dict[actual_key] = doc[actual_key]
upsert = self._created
if updates:
collection.update(select_dict, {"$set": updates}, upsert=upsert, safe=safe, **write_options)
if removals:
collection.update(select_dict, {"$unset": removals}, upsert=upsert, safe=safe, **write_options)
cascade = self._meta.get('cascade', True) if cascade is None else cascade
if cascade:
kwargs = {
"safe": safe,
"force_insert": force_insert,
"validate": validate,
"write_options": write_options,
"cascade": cascade
}
if cascade_kwargs: # Allow granular control over cascades
kwargs.update(cascade_kwargs)
kwargs['_refs'] = _refs
#self._changed_fields = []
self.cascade_save(**kwargs)
except pymongo.errors.OperationFailure, err:
message = 'Could not save document (%s)'
@@ -184,21 +237,33 @@ class Document(BaseDocument):
id_field = self._meta['id_field']
self[id_field] = self._fields[id_field].to_python(object_id)
def reset_changed_fields(doc, inspected_docs=None):
"""Loop through and reset changed fields lists"""
self._changed_fields = []
self._created = False
signals.post_save.send(self.__class__, document=self, created=created)
return self
inspected_docs = inspected_docs or []
inspected_docs.append(doc)
if hasattr(doc, '_changed_fields'):
doc._changed_fields = []
def cascade_save(self, *args, **kwargs):
"""Recursively saves any references / generic references on an object"""
from fields import ReferenceField, GenericReferenceField
_refs = kwargs.get('_refs', []) or []
for field_name in doc._fields:
field = getattr(doc, field_name)
if field not in inspected_docs and hasattr(field, '_changed_fields'):
reset_changed_fields(field, inspected_docs)
for name, cls in self._fields.items():
reset_changed_fields(self)
signals.post_save.send(self.__class__, document=self, created=creation_mode)
if not isinstance(cls, (ReferenceField, GenericReferenceField)):
continue
ref = getattr(self, name)
if not ref:
continue
if isinstance(ref, DBRef):
continue
ref_id = "%s,%s" % (ref.__class__.__name__, str(ref._data))
if ref and ref_id not in _refs:
_refs.append(ref_id)
kwargs["_refs"] = _refs
ref.save(**kwargs)
ref._changed_fields = []
def update(self, **kwargs):
"""Performs an update on the :class:`~mongoengine.Document`
@@ -210,7 +275,12 @@ class Document(BaseDocument):
if not self.pk:
raise OperationError('attempt to update a document not yet saved')
return self.__class__.objects(pk=self.pk).update_one(**kwargs)
# Need to add shard key to query, or you get an error
select_dict = {'pk': self.pk}
shard_key = self.__class__._meta.get('shard_key', tuple())
for k in shard_key:
select_dict[k] = getattr(self, k)
return self.__class__.objects(**select_dict).update_one(**kwargs)
def delete(self, safe=False):
"""Delete the :class:`~mongoengine.Document` from the database. This
@@ -220,10 +290,8 @@ class Document(BaseDocument):
"""
signals.pre_delete.send(self.__class__, document=self)
id_field = self._meta['id_field']
object_id = self._fields[id_field].to_mongo(self[id_field])
try:
self.__class__.objects(**{id_field: object_id}).delete(safe=safe)
self.__class__.objects(pk=self.pk).delete(safe=safe)
except pymongo.errors.OperationFailure, err:
message = u'Could not delete document (%s)' % err.message
raise OperationError(message)
@@ -231,47 +299,54 @@ class Document(BaseDocument):
signals.post_delete.send(self.__class__, document=self)
def select_related(self, max_depth=1):
"""Handles dereferencing of :class:`~pymongo.dbref.DBRef` objects to
"""Handles dereferencing of :class:`~bson.dbref.DBRef` objects to
a maximum depth in order to cut down the number queries to mongodb.
.. versionadded:: 0.5
"""
from dereference import dereference
self._data = dereference(self._data, max_depth)
from dereference import DeReference
self._data = DeReference()(self._data, max_depth)
return self
def reload(self):
def reload(self, max_depth=1):
"""Reloads all attributes from the database.
.. versionadded:: 0.1.2
.. versionchanged:: 0.6 Now chainable
"""
id_field = self._meta['id_field']
obj = self.__class__.objects(**{id_field: self[id_field]}).first()
obj = self.__class__.objects(
**{id_field: self[id_field]}
).first().select_related(max_depth=max_depth)
for field in self._fields:
setattr(self, field, self._reload(field, obj[field]))
self._changed_fields = []
if self._dynamic:
for name in self._dynamic_fields.keys():
setattr(self, name, self._reload(name, obj._data[name]))
self._changed_fields = obj._changed_fields
return obj
def _reload(self, key, value):
"""Used by :meth:`~mongoengine.Document.reload` to ensure the
correct instance is linked to self.
"""
if isinstance(value, BaseDict):
value = [(k, self._reload(k,v)) for k,v in value.items()]
value = BaseDict(value, instance=self, name=key)
value = [(k, self._reload(k, v)) for k, v in value.items()]
value = BaseDict(value, self, key)
elif isinstance(value, BaseList):
value = [self._reload(key, v) for v in value]
value = BaseList(value, instance=self, name=key)
elif isinstance(value, EmbeddedDocument):
value = BaseList(value, self, key)
elif isinstance(value, (EmbeddedDocument, DynamicEmbeddedDocument)):
value._changed_fields = []
return value
def to_dbref(self):
"""Returns an instance of :class:`~pymongo.dbref.DBRef` useful in
"""Returns an instance of :class:`~bson.dbref.DBRef` useful in
`__raw__` queries."""
if not self.pk:
msg = "Only saved documents can have a valid dbref"
raise OperationError(msg)
return pymongo.dbref.DBRef(self.__class__._get_collection_name(), self.pk)
return DBRef(self.__class__._get_collection_name(), self.pk)
@classmethod
def register_delete_rule(cls, document_cls, field_name, rule):
@@ -285,8 +360,52 @@ class Document(BaseDocument):
"""Drops the entire collection associated with this
:class:`~mongoengine.Document` type from the database.
"""
db = _get_db()
from mongoengine.queryset import QuerySet
db = cls._get_db()
db.drop_collection(cls._get_collection_name())
QuerySet._reset_already_indexed(cls)
class DynamicDocument(Document):
"""A Dynamic Document class allowing flexible, expandable and uncontrolled
schemas. As a :class:`~mongoengine.Document` subclass, acts in the same
way as an ordinary document but has expando style properties. Any data
passed or set against the :class:`~mongoengine.DynamicDocument` that is
not a field is automatically converted into a
:class:`~mongoengine.DynamicField` and data can be attributed to that
field.
..note::
There is one caveat on Dynamic Documents: fields cannot start with `_`
"""
__metaclass__ = TopLevelDocumentMetaclass
_dynamic = True
def __delattr__(self, *args, **kwargs):
"""Deletes the attribute by setting to None and allowing _delta to unset
it"""
field_name = args[0]
if field_name in self._dynamic_fields:
setattr(self, field_name, None)
else:
super(DynamicDocument, self).__delattr__(*args, **kwargs)
class DynamicEmbeddedDocument(EmbeddedDocument):
"""A Dynamic Embedded Document class allowing flexible, expandable and
uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more
information about dynamic documents.
"""
__metaclass__ = DocumentMetaclass
_dynamic = True
def __delattr__(self, *args, **kwargs):
"""Deletes the attribute by setting to None and allowing _delta to unset
it"""
field_name = args[0]
setattr(self, field_name, None)
class MapReduceDocument(object):
@@ -294,7 +413,7 @@ class MapReduceDocument(object):
:param collection: An instance of :class:`~pymongo.Collection`
:param key: Document/result key, often an instance of
:class:`~pymongo.objectid.ObjectId`. If supplied as
: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.

View File

@@ -1,27 +1,39 @@
from base import (BaseField, ComplexBaseField, ObjectIdField,
ValidationError, get_document)
from queryset import DO_NOTHING
from document import Document, EmbeddedDocument
from connection import _get_db
from operator import itemgetter
import re
import pymongo
import pymongo.dbref
import pymongo.son
import pymongo.binary
import datetime, time
import datetime
import time
import decimal
import gridfs
import re
import uuid
from bson import Binary, DBRef, SON, ObjectId
from base import (BaseField, ComplexBaseField, ObjectIdField,
ValidationError, get_document, BaseDocument)
from queryset import DO_NOTHING, QuerySet
from document import Document, EmbeddedDocument
from connection import get_db, DEFAULT_CONNECTION_NAME
from operator import itemgetter
try:
from PIL import Image, ImageOps
except ImportError:
Image = None
ImageOps = None
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField',
'ObjectIdField', 'ReferenceField', 'ValidationError', 'MapField',
'DecimalField', 'ComplexDateTimeField', 'URLField',
'DecimalField', 'ComplexDateTimeField', 'URLField', 'DynamicField',
'GenericReferenceField', 'FileField', 'BinaryField',
'SortedListField', 'EmailField', 'GeoPointField',
'SequenceField', 'GenericEmbeddedDocumentField']
'SortedListField', 'EmailField', 'GeoPointField', 'ImageField',
'SequenceField', 'UUIDField', 'GenericEmbeddedDocumentField']
RECURSIVE_REFERENCE_CONSTANT = 'self'
@@ -37,20 +49,23 @@ class StringField(BaseField):
super(StringField, self).__init__(**kwargs)
def to_python(self, value):
return unicode(value)
if isinstance(value, unicode):
return value
else:
return value.decode('utf-8')
def validate(self, value):
assert isinstance(value, (str, unicode))
if not isinstance(value, basestring):
self.error('StringField only accepts string values')
if self.max_length is not None and len(value) > self.max_length:
raise ValidationError('String value is too long')
self.error('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')
self.error('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)
self.error('String value did not match validation regex')
def lookup_member(self, member_name):
return None
@@ -100,16 +115,15 @@ class URLField(StringField):
def validate(self, value):
if not URLField.URL_REGEX.match(value):
raise ValidationError('Invalid URL: %s' % value)
self.error('Invalid URL: %s' % value)
if self.verify_exists:
import urllib2
try:
request = urllib2.Request(value)
response = urllib2.urlopen(request)
urllib2.urlopen(request)
except Exception, e:
message = 'This URL appears to be a broken link: %s' % e
raise ValidationError(message)
self.error('This URL appears to be a broken link: %s' % e)
class EmailField(StringField):
@@ -126,7 +140,7 @@ class EmailField(StringField):
def validate(self, value):
if not EmailField.EMAIL_REGEX.match(value):
raise ValidationError('Invalid Mail-address: %s' % value)
self.error('Invalid Mail-address: %s' % value)
class IntField(BaseField):
@@ -144,15 +158,18 @@ class IntField(BaseField):
try:
value = int(value)
except:
raise ValidationError('%s could not be converted to int' % value)
self.error('%s could not be converted to int' % value)
if self.min_value is not None and value < self.min_value:
raise ValidationError('Integer value is too small')
self.error('Integer value is too small')
if self.max_value is not None and value > self.max_value:
raise ValidationError('Integer value is too large')
self.error('Integer value is too large')
def prepare_query_value(self, op, value):
if value is None:
return value
return int(value)
@@ -170,15 +187,19 @@ class FloatField(BaseField):
def validate(self, value):
if isinstance(value, int):
value = float(value)
assert isinstance(value, float)
if not isinstance(value, float):
self.error('FloatField only accepts float values')
if self.min_value is not None and value < self.min_value:
raise ValidationError('Float value is too small')
self.error('Float value is too small')
if self.max_value is not None and value > self.max_value:
raise ValidationError('Float value is too large')
self.error('Float value is too large')
def prepare_query_value(self, op, value):
if value is None:
return value
return float(value)
@@ -207,13 +228,13 @@ class DecimalField(BaseField):
try:
value = decimal.Decimal(value)
except Exception, exc:
raise ValidationError('Could not convert to decimal: %s' % exc)
self.error('Could not convert value to decimal: %s' % exc)
if self.min_value is not None and value < self.min_value:
raise ValidationError('Decimal value is too small')
self.error('Decimal value is too small')
if self.max_value is not None and value > self.max_value:
raise ValidationError('Decimal value is too large')
self.error('Decimal value is too large')
class BooleanField(BaseField):
@@ -226,7 +247,8 @@ class BooleanField(BaseField):
return bool(value)
def validate(self, value):
assert isinstance(value, bool)
if not isinstance(value, bool):
self.error('BooleanField only accepts boolean values')
class DateTimeField(BaseField):
@@ -239,7 +261,8 @@ class DateTimeField(BaseField):
"""
def validate(self, value):
assert isinstance(value, (datetime.datetime, datetime.date))
if not isinstance(value, (datetime.datetime, datetime.date)):
self.error(u'cannot parse date "%s"' % value)
def to_mongo(self, value):
return self.prepare_query_value(None, value)
@@ -355,13 +378,13 @@ class ComplexDateTimeField(StringField):
return self._convert_from_string(data)
def __set__(self, instance, value):
value = self._convert_from_datetime(value)
value = self._convert_from_datetime(value) if value else value
return super(ComplexDateTimeField, self).__set__(instance, value)
def validate(self, value):
if not isinstance(value, datetime.datetime):
raise ValidationError('Only datetime objects may used in a \
ComplexDateTimeField')
self.error('Only datetime objects may used in a '
'ComplexDateTimeField')
def to_python(self, value):
return self._convert_from_string(value)
@@ -381,8 +404,8 @@ class EmbeddedDocumentField(BaseField):
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.error('Invalid embedded document class provided to an '
'EmbeddedDocumentField')
self.document_type_obj = document_type
super(EmbeddedDocumentField, self).__init__(**kwargs)
@@ -411,8 +434,8 @@ class EmbeddedDocumentField(BaseField):
"""
# Using isinstance also works for subclasses of self.document
if not isinstance(value, self.document_type):
raise ValidationError('Invalid embedded document instance '
'provided to an EmbeddedDocumentField')
self.error('Invalid embedded document instance provided to an '
'EmbeddedDocumentField')
self.document_type.validate(value)
def lookup_member(self, member_name):
@@ -427,6 +450,9 @@ class GenericEmbeddedDocumentField(BaseField):
:class:`~mongoengine.EmbeddedDocument` to be stored.
Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`.
..note :: You can use the choices param to limit the acceptable
EmbeddedDocument types
"""
def prepare_query_value(self, op, value):
@@ -441,8 +467,8 @@ class GenericEmbeddedDocumentField(BaseField):
def validate(self, value):
if not isinstance(value, EmbeddedDocument):
raise ValidationError('Invalid embedded document instance '
'provided to an GenericEmbeddedDocumentField')
self.error('Invalid embedded document instance provided to an '
'GenericEmbeddedDocumentField')
value.validate()
@@ -456,9 +482,56 @@ class GenericEmbeddedDocumentField(BaseField):
return data
class DynamicField(BaseField):
"""A tryly dynamic field type capable of handling different and varying
types of data.
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
def to_mongo(self, value):
"""Convert a Python type to a MongoDBcompatible type.
"""
if isinstance(value, basestring):
return value
if hasattr(value, 'to_mongo'):
return value.to_mongo()
if not isinstance(value, (dict, list, tuple)):
return value
is_list = False
if not hasattr(value, 'items'):
is_list = True
value = dict([(k, v) for k, v in enumerate(value)])
data = {}
for k, v in value.items():
data[k] = self.to_mongo(v)
if is_list: # Convert back to a list
value = [v for k, v in sorted(data.items(), key=itemgetter(0))]
else:
value = data
return value
def lookup_member(self, member_name):
return member_name
def prepare_query_value(self, op, value):
if isinstance(value, basestring):
from mongoengine.fields import StringField
return StringField().prepare_query_value(op, value)
return self.to_mongo(value)
class ListField(ComplexBaseField):
"""A list field that wraps a standard field, allowing multiple instances
of the field to be used as a list in the database.
.. note::
Required means it cannot be empty - as the default for ListFields is []
"""
# ListFields cannot be indexed with _types - MongoDB doesn't support this
@@ -472,14 +545,15 @@ class ListField(ComplexBaseField):
def validate(self, value):
"""Make sure that a list of valid fields is being used.
"""
if not isinstance(value, (list, tuple)):
raise ValidationError('Only lists and tuples may be used in a '
'list field')
if (not isinstance(value, (list, tuple, QuerySet)) or
isinstance(value, basestring)):
self.error('Only lists and tuples may be used in a list field')
super(ListField, self).validate(value)
def prepare_query_value(self, op, value):
if self.field:
if op in ('set', 'unset') and (not isinstance(value, basestring)
and not isinstance(value, BaseDocument)
and hasattr(value, '__iter__')):
return [self.field.prepare_query_value(op, v) for v in value]
return self.field.prepare_query_value(op, value)
@@ -491,27 +565,40 @@ class SortedListField(ListField):
the database in order to ensure that a sorted list is always
retrieved.
.. warning::
There is a potential race condition when handling lists. If you set /
save the whole list then other processes trying to save the whole list
as well could overwrite changes. The safest way to append to a list is
to perform a push operation.
.. versionadded:: 0.4
.. versionchanged:: 0.6 - added reverse keyword
"""
_ordering = None
_order_reverse = False
def __init__(self, field, **kwargs):
if 'ordering' in kwargs.keys():
self._ordering = kwargs.pop('ordering')
if 'reverse' in kwargs.keys():
self._order_reverse = kwargs.pop('reverse')
super(SortedListField, self).__init__(field, **kwargs)
def to_mongo(self, value):
value = super(SortedListField, self).to_mongo(value)
if self._ordering is not None:
return sorted(value, key=itemgetter(self._ordering))
return sorted(value)
return sorted(value, key=itemgetter(self._ordering), reverse=self._order_reverse)
return sorted(value, reverse=self._order_reverse)
class DictField(ComplexBaseField):
"""A dictionary field that wraps a standard Python dictionary. This is
similar to an embedded document, but the structure is not defined.
.. note::
Required means it cannot be empty - as the default for ListFields is []
.. versionadded:: 0.3
.. versionchanged:: 0.5 - Can now handle complex / varying types of data
"""
@@ -519,7 +606,8 @@ class DictField(ComplexBaseField):
def __init__(self, basecls=None, field=None, *args, **kwargs):
self.field = field
self.basecls = basecls or BaseField
assert issubclass(self.basecls, BaseField)
if not issubclass(self.basecls, BaseField):
self.error('DictField only accepts dict values')
kwargs.setdefault('default', lambda: {})
super(DictField, self).__init__(*args, **kwargs)
@@ -527,12 +615,13 @@ class DictField(ComplexBaseField):
"""Make sure that a list of valid fields is being used.
"""
if not isinstance(value, dict):
raise ValidationError('Only dictionaries may be used in a '
'DictField')
self.error('Only dictionaries may be used in a DictField')
if any(('.' in k or '$' in k) for k in value):
raise ValidationError('Invalid dictionary key name - keys may not '
'contain "." or "$" characters')
if any(k for k in value.keys() if not isinstance(k, basestring)):
self.error('Invalid dictionary key - documents must have only string keys')
if any(('.' in k or '$' in k) for k in value.keys()):
self.error('Invalid dictionary key name - keys may not contain "."'
' or "$" characters')
super(DictField, self).validate(value)
def lookup_member(self, member_name):
@@ -559,18 +648,19 @@ class MapField(DictField):
def __init__(self, field=None, *args, **kwargs):
if not isinstance(field, BaseField):
raise ValidationError('Argument to MapField constructor must be '
'a valid field')
self.error('Argument to MapField constructor must be a valid '
'field')
super(MapField, self).__init__(field=field, *args, **kwargs)
class ReferenceField(BaseField):
"""A reference to a document that will be automatically dereferenced on
access (lazily).
Use the `reverse_delete_rule` to handle what should happen if the document
the field is referencing is deleted.
the field is referencing is deleted. EmbeddedDocuments, DictFields and
MapFields do not support reverse_delete_rules and an `InvalidDocumentError`
will be raised if trying to set on one of these Document / Field types.
The options are:
@@ -578,6 +668,18 @@ class ReferenceField(BaseField):
* NULLIFY - Updates the reference to null.
* CASCADE - Deletes the documents associated with the reference.
* DENY - Prevent the deletion of the reference object.
* PULL - Pull the reference from a :class:`~mongoengine.ListField` of references
Alternative syntax for registering delete rules (useful when implementing
bi-directional delete rules)
.. code-block:: python
class Bar(Document):
content = StringField()
foo = ReferenceField('Foo')
Bar.register_delete_rule(Foo, 'bar', NULLIFY)
.. versionchanged:: 0.5 added `reverse_delete_rule`
"""
@@ -590,8 +692,8 @@ class ReferenceField(BaseField):
"""
if not isinstance(document_type, basestring):
if not issubclass(document_type, (Document, basestring)):
raise ValidationError('Argument to ReferenceField constructor '
'must be a document class or a string')
self.error('Argument to ReferenceField constructor must be a '
'document class or a string')
self.document_type_obj = document_type
self.reverse_delete_rule = reverse_delete_rule
super(ReferenceField, self).__init__(**kwargs)
@@ -615,14 +717,17 @@ class ReferenceField(BaseField):
# Get value from document instance if available
value = instance._data.get(self.name)
# Dereference DBRefs
if isinstance(value, (pymongo.dbref.DBRef)):
value = _get_db().dereference(value)
if isinstance(value, (DBRef)):
value = self.document_type._get_db().dereference(value)
if value is not None:
instance._data[self.name] = self.document_type._from_son(value)
return super(ReferenceField, self).__get__(instance, owner)
def to_mongo(self, document):
if isinstance(document, DBRef):
return document
id_field_name = self.document_type._meta['id_field']
id_field = self.document_type._fields[id_field_name]
@@ -630,25 +735,28 @@ class ReferenceField(BaseField):
# We need the id from the saved object to create the DBRef
id_ = document.id
if id_ is None:
raise ValidationError('You can only reference documents once '
'they have been saved to the database')
self.error('You can only reference documents once they have'
' been saved to the database')
else:
id_ = document
id_ = id_field.to_mongo(id_)
collection = self.document_type._get_collection_name()
return pymongo.dbref.DBRef(collection, id_)
return DBRef(collection, id_)
def prepare_query_value(self, op, value):
if value is None:
return None
return self.to_mongo(value)
def validate(self, value):
assert isinstance(value, (self.document_type, pymongo.dbref.DBRef))
if not isinstance(value, (self.document_type, DBRef)):
self.error('A ReferenceField only accepts DBRef')
if isinstance(value, Document) and value.id is None:
raise ValidationError('You can only reference documents once '
'they have been saved to the database')
self.error('You can only reference documents once they have been '
'saved to the database')
def lookup_member(self, member_name):
return self.document_type._fields.get(member_name)
@@ -661,6 +769,8 @@ class GenericReferenceField(BaseField):
..note :: Any documents used as a generic reference must be registered in the
document registry. Importing the model will automatically register it.
..note :: You can use the choices param to limit the acceptable Document types
.. versionadded:: 0.3
"""
@@ -669,24 +779,24 @@ class GenericReferenceField(BaseField):
return self
value = instance._data.get(self.name)
if isinstance(value, (dict, pymongo.son.SON)):
if isinstance(value, (dict, SON)):
instance._data[self.name] = self.dereference(value)
return super(GenericReferenceField, self).__get__(instance, owner)
def validate(self, value):
if not isinstance(value, (Document, pymongo.dbref.DBRef)):
raise ValidationError('GenericReferences can only contain documents')
if not isinstance(value, (Document, DBRef)):
self.error('GenericReferences can only contain documents')
# We need the id from the saved object to create the DBRef
if isinstance(value, Document) and value.id is None:
raise ValidationError('You can only reference documents once '
'they have been saved to the database')
self.error('You can only reference documents once they have been'
' saved to the database')
def dereference(self, value):
doc_cls = get_document(value['_cls'])
reference = value['_ref']
doc = _get_db().dereference(reference)
doc = doc_cls._get_db().dereference(reference)
if doc is not None:
doc = doc_cls._from_son(doc)
return doc
@@ -695,6 +805,9 @@ class GenericReferenceField(BaseField):
if document is None:
return None
if isinstance(document, (dict, SON)):
return document
id_field_name = document.__class__._meta['id_field']
id_field = document.__class__._fields[id_field_name]
@@ -702,17 +815,20 @@ class GenericReferenceField(BaseField):
# We need the id from the saved object to create the DBRef
id_ = document.id
if id_ is None:
raise ValidationError('You can only reference documents once '
'they have been saved to the database')
self.error('You can only reference documents once they have'
' been saved to the database')
else:
id_ = document
id_ = id_field.to_mongo(id_)
collection = document._get_collection_name()
ref = pymongo.dbref.DBRef(collection, id_)
ref = DBRef(collection, id_)
return {'_cls': document._class_name, '_ref': ref}
def prepare_query_value(self, op, value):
if value is None:
return None
return self.to_mongo(value)
@@ -725,17 +841,17 @@ class BinaryField(BaseField):
super(BinaryField, self).__init__(**kwargs)
def to_mongo(self, value):
return pymongo.binary.Binary(value)
return Binary(value)
def to_python(self, value):
# Returns str not unicode as this is binary data
return str(value)
return "%s" % value
def validate(self, value):
assert isinstance(value, str)
if not isinstance(value, basestring):
self.error('BinaryField only accepts string values')
if self.max_bytes is not None and len(value) > self.max_bytes:
raise ValidationError('Binary value is too long')
self.error('Binary value is too long')
class GridFSError(Exception):
@@ -747,17 +863,28 @@ class GridFSProxy(object):
.. versionadded:: 0.4
.. versionchanged:: 0.5 - added optional size param to read
.. versionchanged:: 0.6 - added collection name param
"""
def __init__(self, grid_id=None, key=None, instance=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
self.gridout = None
_fs = None
def __init__(self, grid_id=None, key=None,
instance=None,
db_alias=DEFAULT_CONNECTION_NAME,
collection_name='fs'):
self.grid_id = grid_id # Store GridFS id for file
self.key = key
self.instance = instance
self.db_alias = db_alias
self.collection_name = collection_name
self.newfile = None # Used for partial writes
self.gridout = None
def __getattr__(self, name):
attrs = ('_fs', 'grid_id', 'key', 'instance', 'db_alias',
'collection_name', 'newfile', 'gridout')
if name in attrs:
return self.__getattribute__(name)
obj = self.get()
if name in dir(obj):
return getattr(obj, name)
@@ -769,6 +896,24 @@ class GridFSProxy(object):
def __nonzero__(self):
return bool(self.grid_id)
def __getstate__(self):
self_dict = self.__dict__
self_dict['_fs'] = None
return self_dict
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.grid_id)
def __cmp__(self, other):
return cmp((self.grid_id, self.collection_name, self.db_alias),
(other.grid_id, other.collection_name, other.db_alias))
@property
def fs(self):
if not self._fs:
self._fs = gridfs.GridFS(get_db(self.db_alias), self.collection_name)
return self._fs
def get(self, id=None):
if id:
self.grid_id = id
@@ -809,10 +954,14 @@ class GridFSProxy(object):
self.newfile.writelines(lines)
def read(self, size=-1):
try:
return self.get().read(size)
except:
gridout = self.get()
if gridout is None:
return None
else:
try:
return gridout.read(size)
except:
return ""
def delete(self):
# Delete file from GridFS, FileField still remains
@@ -840,10 +989,16 @@ class FileField(BaseField):
.. versionadded:: 0.4
.. versionchanged:: 0.5 added optional size param for read
.. versionchanged:: 0.6 added db_alias for multidb support
"""
proxy_class = GridFSProxy
def __init__(self, **kwargs):
def __init__(self,
db_alias=DEFAULT_CONNECTION_NAME,
collection_name="fs", **kwargs):
super(FileField, self).__init__(**kwargs)
self.collection_name = collection_name
self.db_alias = db_alias
def __get__(self, instance, owner):
if instance is None:
@@ -851,17 +1006,20 @@ class FileField(BaseField):
# Check if a file already exists for this model
grid_file = instance._data.get(self.name)
self.grid_file = grid_file
if isinstance(self.grid_file, GridFSProxy):
if not self.grid_file.key:
self.grid_file.key = self.name
self.grid_file.instance = instance
return self.grid_file
return GridFSProxy(key=self.name, instance=instance)
if not isinstance(grid_file, self.proxy_class):
grid_file = self.proxy_class(key=self.name, instance=instance,
db_alias=self.db_alias,
collection_name=self.collection_name)
instance._data[self.name] = grid_file
if not grid_file.key:
grid_file.key = self.name
grid_file.instance = instance
return grid_file
def __set__(self, instance, value):
key = self.name
if isinstance(value, file) or isinstance(value, str):
if (hasattr(value, 'read') and not isinstance(value, GridFSProxy)) or isinstance(value, basestring):
# using "FileField() = file/string" notation
grid_file = instance._data.get(self.name)
# If a file already exists, delete it
@@ -874,7 +1032,8 @@ class FileField(BaseField):
grid_file.put(value)
else:
# Create a new proxy object as we don't already have one
instance._data[key] = GridFSProxy(key=key, instance=instance)
instance._data[key] = self.proxy_class(key=key, instance=instance,
collection_name=self.collection_name)
instance._data[key].put(value)
else:
instance._data[key] = value
@@ -883,18 +1042,181 @@ class FileField(BaseField):
def to_mongo(self, value):
# Store the GridFS file id in MongoDB
if isinstance(value, GridFSProxy) and value.grid_id is not None:
if isinstance(value, self.proxy_class) 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)
return self.proxy_class(value,
collection_name=self.collection_name,
db_alias=self.db_alias)
def validate(self, value):
if value.grid_id is not None:
assert isinstance(value, GridFSProxy)
assert isinstance(value.grid_id, pymongo.objectid.ObjectId)
if not isinstance(value, self.proxy_class):
self.error('FileField only accepts GridFSProxy values')
if not isinstance(value.grid_id, ObjectId):
self.error('Invalid GridFSProxy value')
class ImageGridFsProxy(GridFSProxy):
"""
Proxy for ImageField
versionadded: 0.6
"""
def put(self, file_obj, **kwargs):
"""
Insert a image in database
applying field properties (size, thumbnail_size)
"""
field = self.instance._fields[self.key]
try:
img = Image.open(file_obj)
except:
raise ValidationError('Invalid image')
if (field.size and (img.size[0] > field.size['width'] or
img.size[1] > field.size['height'])):
size = field.size
if size['force']:
img = ImageOps.fit(img,
(size['width'],
size['height']),
Image.ANTIALIAS)
else:
img.thumbnail((size['width'],
size['height']),
Image.ANTIALIAS)
thumbnail = None
if field.thumbnail_size:
size = field.thumbnail_size
if size['force']:
thumbnail = ImageOps.fit(img,
(size['width'],
size['height']),
Image.ANTIALIAS)
else:
thumbnail = img.copy()
thumbnail.thumbnail((size['width'],
size['height']),
Image.ANTIALIAS)
if thumbnail:
thumb_id = self._put_thumbnail(thumbnail,
img.format)
else:
thumb_id = None
w, h = img.size
io = StringIO()
img.save(io, img.format)
io.seek(0)
return super(ImageGridFsProxy, self).put(io,
width=w,
height=h,
format=img.format,
thumbnail_id=thumb_id,
**kwargs)
def delete(self, *args, **kwargs):
#deletes thumbnail
out = self.get()
if out and out.thumbnail_id:
self.fs.delete(out.thumbnail_id)
return super(ImageGridFsProxy, self).delete(*args, **kwargs)
def _put_thumbnail(self, thumbnail, format, **kwargs):
w, h = thumbnail.size
io = StringIO()
thumbnail.save(io, format)
io.seek(0)
return self.fs.put(io, width=w,
height=h,
format=format,
**kwargs)
@property
def size(self):
"""
return a width, height of image
"""
out = self.get()
if out:
return out.width, out.height
@property
def format(self):
"""
return format of image
ex: PNG, JPEG, GIF, etc
"""
out = self.get()
if out:
return out.format
@property
def thumbnail(self):
"""
return a gridfs.grid_file.GridOut
representing a thumbnail of Image
"""
out = self.get()
if out and out.thumbnail_id:
return self.fs.get(out.thumbnail_id)
def write(self, *args, **kwargs):
raise RuntimeError("Please use \"put\" method instead")
def writelines(self, *args, **kwargs):
raise RuntimeError("Please use \"put\" method instead")
class ImproperlyConfigured(Exception):
pass
class ImageField(FileField):
"""
A Image File storage field.
@size (width, height, force):
max size to store images, if larger will be automatically resized
ex: size=(800, 600, True)
@thumbnail (width, height, force):
size to generate a thumbnail
.. versionadded:: 0.6
"""
proxy_class = ImageGridFsProxy
def __init__(self, size=None, thumbnail_size=None,
collection_name='images', **kwargs):
if not Image:
raise ImproperlyConfigured("PIL library was not found")
params_size = ('width', 'height', 'force')
extra_args = dict(size=size, thumbnail_size=thumbnail_size)
for att_name, att in extra_args.items():
if att and (isinstance(att, tuple) or isinstance(att, list)):
setattr(self, att_name, dict(
map(None, params_size, att)))
else:
setattr(self, att_name, None)
super(ImageField, self).__init__(
collection_name=collection_name,
**kwargs)
class GeoPointField(BaseField):
@@ -909,14 +1231,14 @@ class GeoPointField(BaseField):
"""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)')
self.error('GeoPointField can only accept tuples or lists '
'of (x, y)')
if not len(value) == 2:
raise ValidationError('Value must be a two-dimensional point.')
self.error('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.')
self.error('Both values in point must be float or int')
class SequenceField(IntField):
@@ -932,8 +1254,9 @@ class SequenceField(IntField):
.. versionadded:: 0.5
"""
def __init__(self, collection_name=None, *args, **kwargs):
def __init__(self, collection_name=None, db_alias = None, *args, **kwargs):
self.collection_name = collection_name or 'mongoengine.counters'
self.db_alias = db_alias or DEFAULT_CONNECTION_NAME
return super(SequenceField, self).__init__(*args, **kwargs)
def generate_new_value(self):
@@ -942,7 +1265,7 @@ class SequenceField(IntField):
"""
sequence_id = "{0}.{1}".format(self.owner_document._get_collection_name(),
self.name)
collection = _get_db()[self.collection_name]
collection = get_db(alias = self.db_alias )[self.collection_name]
counter = collection.find_and_modify(query={"_id": sequence_id},
update={"$inc": {"next": 1}},
new=True,
@@ -977,3 +1300,30 @@ class SequenceField(IntField):
if value is None:
value = self.generate_new_value()
return value
class UUIDField(BaseField):
"""A UUID field.
.. versionadded:: 0.6
"""
def __init__(self, **kwargs):
super(UUIDField, self).__init__(**kwargs)
def to_python(self, value):
if not isinstance(value, basestring):
value = unicode(value)
return uuid.UUID(value)
def to_mongo(self, value):
return unicode(value)
def validate(self, value):
if not isinstance(value, uuid.UUID):
if not isinstance(value, basestring):
value = str(value)
try:
value = uuid.UUID(value)
except Exception, exc:
self.error('Could not convert to UUID: %s' % exc)

File diff suppressed because it is too large Load Diff

View File

@@ -42,3 +42,5 @@ pre_save = _signals.signal('pre_save')
post_save = _signals.signal('post_save')
pre_delete = _signals.signal('pre_delete')
post_delete = _signals.signal('post_delete')
pre_bulk_insert = _signals.signal('pre_bulk_insert')
post_bulk_insert = _signals.signal('post_bulk_insert')

View File

@@ -1,4 +1,4 @@
from mongoengine.connection import _get_db
from mongoengine.connection import get_db
class query_counter(object):
@@ -7,7 +7,7 @@ class query_counter(object):
def __init__(self):
""" Construct the query_counter. """
self.counter = 0
self.db = _get_db()
self.db = get_db()
def __enter__(self):
""" On every with block we need to drop the profile collection. """

54
python-mongoengine.spec Normal file
View File

@@ -0,0 +1,54 @@
# sitelib for noarch packages, sitearch for others (remove the unneeded one)
%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")}
%define srcname mongoengine
Name: python-%{srcname}
Version: 0.6.16
Release: 1%{?dist}
Summary: A Python Document-Object Mapper for working with MongoDB
Group: Development/Libraries
License: MIT
URL: https://github.com/MongoEngine/mongoengine
Source0: %{srcname}-%{version}.tar.bz2
BuildRequires: python-devel
BuildRequires: python-setuptools
Requires: mongodb
Requires: pymongo
Requires: python-blinker
Requires: python-imaging
%description
MongoEngine is an ORM-like layer on top of PyMongo.
%prep
%setup -q -n %{srcname}-%{version}
%build
# Remove CFLAGS=... for noarch packages (unneeded)
CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
%clean
rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root,-)
%doc docs AUTHORS LICENSE README.rst
# For noarch packages: sitelib
%{python_sitelib}/*
# For arch-specific packages: sitearch
# %{python_sitearch}/*
%changelog
* See: http://readthedocs.org/docs/mongoengine-odm/en/latest/changelog.html

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
pymongo

13
setup.cfg Normal file
View File

@@ -0,0 +1,13 @@
[aliases]
test = nosetests
[nosetests]
verbosity = 2
detailed-errors = 1
#with-coverage = 1
#cover-erase = 1
#cover-html = 1
#cover-html-dir = ../htmlcov
#cover-package = mongoengine
where = tests
#tests = test_bugfix.py

View File

@@ -35,10 +35,12 @@ CLASSIFIERS = [
setup(name='mongoengine',
version=VERSION,
packages=find_packages(),
packages=find_packages(exclude=('tests',)),
author='Harry Marr',
author_email='harry.marr@{nospam}gmail.com',
url='http://hmarr.com/mongoengine/',
maintainer="Ross Lawley",
maintainer_email="ross.lawley@{nospam}gmail.com",
url='http://mongoengine.org/',
license='MIT',
include_package_data=True,
description=DESCRIPTION,
@@ -46,6 +48,5 @@ setup(name='mongoengine',
platforms=['any'],
classifiers=CLASSIFIERS,
install_requires=['pymongo'],
test_suite='tests',
tests_require=['blinker', 'django==1.3']
tests_require=['nose', 'coverage', 'blinker', 'django>=1.3', 'PIL']
)

View File

@@ -1,9 +1,6 @@
from datetime import datetime
import pymongo
from mongoengine import *
from mongoengine.base import BaseField
from mongoengine.connection import _get_db
class PickleEmbedded(EmbeddedDocument):
@@ -15,6 +12,7 @@ class PickleTest(Document):
string = StringField(choices=(('One', '1'), ('Two', '2')))
embedded = EmbeddedDocumentField(PickleEmbedded)
lists = ListField(StringField())
photo = FileField()
class Mixin(object):
@@ -22,4 +20,4 @@ class Mixin(object):
class Base(Document):
pass
meta = {'allow_inheritance': True}

BIN
tests/mongoengine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

98
tests/test_connection.py Normal file
View File

@@ -0,0 +1,98 @@
import datetime
import pymongo
import unittest
import mongoengine.connection
from bson.tz_util import utc
from mongoengine import *
from mongoengine.connection import get_db, get_connection, ConnectionError
class ConnectionTest(unittest.TestCase):
def tearDown(self):
mongoengine.connection._connection_settings = {}
mongoengine.connection._connections = {}
mongoengine.connection._dbs = {}
def test_connect(self):
"""Ensure that the connect() method works properly.
"""
connect('mongoenginetest')
conn = get_connection()
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
db = get_db()
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest')
connect('mongoenginetest2', alias='testdb')
conn = get_connection('testdb')
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
def test_connect_uri(self):
"""Ensure that the connect() method works properly with uri's
"""
c = connect(db='mongoenginetest', alias='admin')
c.admin.system.users.remove({})
c.mongoenginetest.system.users.remove({})
c.admin.add_user("admin", "password")
c.admin.authenticate("admin", "password")
c.mongoenginetest.add_user("username", "password")
self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost')
connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest')
conn = get_connection()
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
db = get_db()
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest')
def test_register_connection(self):
"""Ensure that connections with different aliases may be registered.
"""
register_connection('testdb', 'mongoenginetest2')
self.assertRaises(ConnectionError, get_connection)
conn = get_connection('testdb')
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
db = get_db('testdb')
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest2')
def test_connection_kwargs(self):
"""Ensure that connection kwargs get passed to pymongo.
"""
connect('mongoenginetest', alias='t1', tz_aware=True)
conn = get_connection('t1')
self.assertTrue(conn.tz_aware)
connect('mongoenginetest2', alias='t2')
conn = get_connection('t2')
self.assertFalse(conn.tz_aware)
def test_datetime(self):
connect('mongoenginetest', tz_aware=True)
d = datetime.datetime(2010, 5, 5, tzinfo=utc)
class DateDoc(Document):
the_date = DateTimeField(required=True)
DateDoc.drop_collection()
DateDoc(the_date=d).save()
date_doc = DateDoc.objects.first()
self.assertEqual(d, date_doc.the_date)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,7 +1,7 @@
import unittest
from mongoengine import *
from mongoengine.connection import _get_db
from mongoengine.connection import get_db
from mongoengine.tests import query_counter
@@ -9,7 +9,7 @@ class FieldTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = _get_db()
self.db = get_db()
def test_list_item_dereference(self):
"""Ensure that DBRef items in ListFields are dereferenced.
@@ -188,6 +188,51 @@ class FieldTest(unittest.TestCase):
self.assertEquals("[<Person: Mother>, <Person: Daughter>]", "%s" % Person.objects())
def test_circular_tree_reference(self):
"""Ensure you can handle circular references with more than one level
"""
class Other(EmbeddedDocument):
name = StringField()
friends = ListField(ReferenceField('Person'))
class Person(Document):
name = StringField()
other = EmbeddedDocumentField(Other, default=lambda: Other())
def __repr__(self):
return "<Person: %s>" % self.name
Person.drop_collection()
paul = Person(name="Paul")
paul.save()
maria = Person(name="Maria")
maria.save()
julia = Person(name='Julia')
julia.save()
anna = Person(name='Anna')
anna.save()
paul.other.friends = [maria, julia, anna]
paul.other.name = "Paul's friends"
paul.save()
maria.other.friends = [paul, julia, anna]
maria.other.name = "Maria's friends"
maria.save()
julia.other.friends = [paul, maria, anna]
julia.other.name = "Julia's friends"
julia.save()
anna.other.friends = [paul, maria, julia]
anna.other.name = "Anna's friends"
anna.save()
self.assertEquals(
"[<Person: Paul>, <Person: Maria>, <Person: Julia>, <Person: Anna>]",
"%s" % Person.objects()
)
def test_generic_reference(self):
class UserA(Document):
@@ -715,3 +760,106 @@ class FieldTest(unittest.TestCase):
UserB.drop_collection()
UserC.drop_collection()
Group.drop_collection()
def test_multidirectional_lists(self):
class Asset(Document):
name = StringField(max_length=250, required=True)
parent = GenericReferenceField(default=None)
parents = ListField(GenericReferenceField())
children = ListField(GenericReferenceField())
Asset.drop_collection()
root = Asset(name='', path="/", title="Site Root")
root.save()
company = Asset(name='company', title='Company', parent=root, parents=[root])
company.save()
root.children = [company]
root.save()
root = root.reload()
self.assertEquals(root.children, [company])
self.assertEquals(company.parents, [root])
def test_dict_in_dbref_instance(self):
class Person(Document):
name = StringField(max_length=250, required=True)
class Room(Document):
number = StringField(max_length=250, required=True)
staffs_with_position = ListField(DictField())
Person.drop_collection()
Room.drop_collection()
bob = Person.objects.create(name='Bob')
bob.save()
sarah = Person.objects.create(name='Sarah')
sarah.save()
room_101 = Room.objects.create(number="101")
room_101.staffs_with_position = [
{'position_key': 'window', 'staff': sarah},
{'position_key': 'door', 'staff': bob.to_dbref()}]
room_101.save()
room = Room.objects.first().select_related()
self.assertEquals(room.staffs_with_position[0]['staff'], sarah)
self.assertEquals(room.staffs_with_position[1]['staff'], bob)
def test_document_reload_no_inheritance(self):
class Foo(Document):
meta = {'allow_inheritance': False}
bar = ReferenceField('Bar')
baz = ReferenceField('Baz')
class Bar(Document):
meta = {'allow_inheritance': False}
msg = StringField(required=True, default='Blammo!')
class Baz(Document):
meta = {'allow_inheritance': False}
msg = StringField(required=True, default='Kaboom!')
Foo.drop_collection()
Bar.drop_collection()
Baz.drop_collection()
bar = Bar()
bar.save()
baz = Baz()
baz.save()
foo = Foo()
foo.bar = bar
foo.baz = baz
foo.save()
foo.reload()
self.assertEquals(type(foo.bar), Bar)
self.assertEquals(type(foo.baz), Baz)
def test_list_lookup_not_checked_in_map(self):
"""Ensure we dereference list data correctly
"""
class Comment(Document):
id = IntField(primary_key=True)
text = StringField()
class Message(Document):
id = IntField(primary_key=True)
comments = ListField(ReferenceField(Comment))
Comment.drop_collection()
Message.drop_collection()
c1 = Comment(id=0, text='zero').save()
c2 = Comment(id=1, text='one').save()
Message(id=1, comments=[c1, c2]).save()
msg = Message.objects.get(id=1)
self.assertEqual(0, msg.comments[0].id)
self.assertEqual(1, msg.comments[1].id)

View File

@@ -8,8 +8,14 @@ from mongoengine.django.shortcuts import get_document_or_404
from django.http import Http404
from django.template import Context, Template
from django.conf import settings
from django.core.paginator import Paginator
settings.configure()
from django.contrib.sessions.tests import SessionTestsMixin
from mongoengine.django.sessions import SessionStore, MongoSession
class QuerySetTest(unittest.TestCase):
def setUp(self):
@@ -67,3 +73,38 @@ class QuerySetTest(unittest.TestCase):
self.assertRaises(Http404, get_document_or_404, self.Person, pk='1234')
self.assertEqual(p, get_document_or_404(self.Person, pk=p.pk))
def test_pagination(self):
"""Ensure that Pagination works as expected
"""
class Page(Document):
name = StringField()
Page.drop_collection()
for i in xrange(1, 11):
Page(name=str(i)).save()
paginator = Paginator(Page.objects.all(), 2)
t = Template("{% for i in page.object_list %}{{ i.name }}:{% endfor %}")
for p in paginator.page_range:
d = {"page": paginator.page(p)}
end = p * 2
start = end - 1
self.assertEqual(t.render(Context(d)), u'%d:%d:' % (start, end))
class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
backend = SessionStore
def setUp(self):
connect(db='mongoenginetest')
MongoSession.drop_collection()
super(MongoDBSessionTest, self).setUp()
def test_first_save(self):
session = SessionStore()
session['test'] = True
session.save()
self.assertTrue('test' in session)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,502 @@
import unittest
from mongoengine import *
from mongoengine.connection import get_db
class DynamicDocTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
class Person(DynamicDocument):
name = StringField()
meta = {'allow_inheritance': True}
Person.drop_collection()
self.Person = Person
def test_simple_dynamic_document(self):
"""Ensures simple dynamic documents are saved correctly"""
p = self.Person()
p.name = "James"
p.age = 34
self.assertEquals(p.to_mongo(),
{"_types": ["Person"], "_cls": "Person",
"name": "James", "age": 34}
)
p.save()
self.assertEquals(self.Person.objects.first().age, 34)
# Confirm no changes to self.Person
self.assertFalse(hasattr(self.Person, 'age'))
def test_dynamic_document_delta(self):
"""Ensures simple dynamic documents can delta correctly"""
p = self.Person(name="James", age=34)
self.assertEquals(p._delta(), ({'_types': ['Person'], 'age': 34, 'name': 'James', '_cls': 'Person'}, {}))
p.doc = 123
del(p.doc)
self.assertEquals(p._delta(), ({'_types': ['Person'], 'age': 34, 'name': 'James', '_cls': 'Person'}, {'doc': 1}))
def test_change_scope_of_variable(self):
"""Test changing the scope of a dynamic field has no adverse effects"""
p = self.Person()
p.name = "Dean"
p.misc = 22
p.save()
p = self.Person.objects.get()
p.misc = {'hello': 'world'}
p.save()
p = self.Person.objects.get()
self.assertEquals(p.misc, {'hello': 'world'})
def test_delete_dynamic_field(self):
"""Test deleting a dynamic field works"""
self.Person.drop_collection()
p = self.Person()
p.name = "Dean"
p.misc = 22
p.save()
p = self.Person.objects.get()
p.misc = {'hello': 'world'}
p.save()
p = self.Person.objects.get()
self.assertEquals(p.misc, {'hello': 'world'})
collection = self.db[self.Person._get_collection_name()]
obj = collection.find_one()
self.assertEquals(sorted(obj.keys()), ['_cls', '_id', '_types', 'misc', 'name'])
del(p.misc)
p.save()
p = self.Person.objects.get()
self.assertFalse(hasattr(p, 'misc'))
obj = collection.find_one()
self.assertEquals(sorted(obj.keys()), ['_cls', '_id', '_types', 'name'])
def test_dynamic_document_queries(self):
"""Ensure we can query dynamic fields"""
p = self.Person()
p.name = "Dean"
p.age = 22
p.save()
self.assertEquals(1, self.Person.objects(age=22).count())
p = self.Person.objects(age=22)
p = p.get()
self.assertEquals(22, p.age)
def test_complex_dynamic_document_queries(self):
class Person(DynamicDocument):
name = StringField()
Person.drop_collection()
p = Person(name="test")
p.age = "ten"
p.save()
p1 = Person(name="test1")
p1.age = "less then ten and a half"
p1.save()
p2 = Person(name="test2")
p2.age = 10
p2.save()
self.assertEquals(Person.objects(age__icontains='ten').count(), 2)
self.assertEquals(Person.objects(age__gte=10).count(), 1)
def test_complex_data_lookups(self):
"""Ensure you can query dynamic document dynamic fields"""
p = self.Person()
p.misc = {'hello': 'world'}
p.save()
self.assertEquals(1, self.Person.objects(misc__hello='world').count())
def test_inheritance(self):
"""Ensure that dynamic document plays nice with inheritance"""
class Employee(self.Person):
salary = IntField()
Employee.drop_collection()
self.assertTrue('name' in Employee._fields)
self.assertTrue('salary' in Employee._fields)
self.assertEqual(Employee._get_collection_name(),
self.Person._get_collection_name())
joe_bloggs = Employee()
joe_bloggs.name = "Joe Bloggs"
joe_bloggs.salary = 10
joe_bloggs.age = 20
joe_bloggs.save()
self.assertEquals(1, self.Person.objects(age=20).count())
self.assertEquals(1, Employee.objects(age=20).count())
joe_bloggs = self.Person.objects.first()
self.assertTrue(isinstance(joe_bloggs, Employee))
def test_embedded_dynamic_document(self):
"""Test dynamic embedded documents"""
class Embedded(DynamicEmbeddedDocument):
pass
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
embedded_1 = Embedded()
embedded_1.string_field = 'hello'
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field = embedded_1
self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc",
"embedded_field": {
"_types": ['Embedded'], "_cls": "Embedded",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ['1', 2, {'hello': 'world'}]
}
})
doc.save()
doc = Doc.objects.first()
self.assertEquals(doc.embedded_field.__class__, Embedded)
self.assertEquals(doc.embedded_field.string_field, "hello")
self.assertEquals(doc.embedded_field.int_field, 1)
self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'})
self.assertEquals(doc.embedded_field.list_field, ['1', 2, {'hello': 'world'}])
def test_complex_embedded_documents(self):
"""Test complex dynamic embedded documents setups"""
class Embedded(DynamicEmbeddedDocument):
pass
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
embedded_1 = Embedded()
embedded_1.string_field = 'hello'
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_2 = Embedded()
embedded_2.string_field = 'hello'
embedded_2.int_field = 1
embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
embedded_1.list_field = ['1', 2, embedded_2]
doc.embedded_field = embedded_1
self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc",
"embedded_field": {
"_types": ['Embedded'], "_cls": "Embedded",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ['1', 2,
{"_types": ['Embedded'], "_cls": "Embedded",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ['1', 2, {'hello': 'world'}]}
]
}
})
doc.save()
doc = Doc.objects.first()
self.assertEquals(doc.embedded_field.__class__, Embedded)
self.assertEquals(doc.embedded_field.string_field, "hello")
self.assertEquals(doc.embedded_field.int_field, 1)
self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'})
self.assertEquals(doc.embedded_field.list_field[0], '1')
self.assertEquals(doc.embedded_field.list_field[1], 2)
embedded_field = doc.embedded_field.list_field[2]
self.assertEquals(embedded_field.__class__, Embedded)
self.assertEquals(embedded_field.string_field, "hello")
self.assertEquals(embedded_field.int_field, 1)
self.assertEquals(embedded_field.dict_field, {'hello': 'world'})
self.assertEquals(embedded_field.list_field, ['1', 2, {'hello': 'world'}])
def test_delta_for_dynamic_documents(self):
p = self.Person()
p.name = "Dean"
p.age = 22
p.save()
p.age = 24
self.assertEquals(p.age, 24)
self.assertEquals(p._get_changed_fields(), ['age'])
self.assertEquals(p._delta(), ({'age': 24}, {}))
p = self.Person.objects(age=22).get()
p.age = 24
self.assertEquals(p.age, 24)
self.assertEquals(p._get_changed_fields(), ['age'])
self.assertEquals(p._delta(), ({'age': 24}, {}))
p.save()
self.assertEquals(1, self.Person.objects(age=24).count())
def test_delta(self):
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEquals(doc._get_changed_fields(), [])
self.assertEquals(doc._delta(), ({}, {}))
doc.string_field = 'hello'
self.assertEquals(doc._get_changed_fields(), ['string_field'])
self.assertEquals(doc._delta(), ({'string_field': 'hello'}, {}))
doc._changed_fields = []
doc.int_field = 1
self.assertEquals(doc._get_changed_fields(), ['int_field'])
self.assertEquals(doc._delta(), ({'int_field': 1}, {}))
doc._changed_fields = []
dict_value = {'hello': 'world', 'ping': 'pong'}
doc.dict_field = dict_value
self.assertEquals(doc._get_changed_fields(), ['dict_field'])
self.assertEquals(doc._delta(), ({'dict_field': dict_value}, {}))
doc._changed_fields = []
list_value = ['1', 2, {'hello': 'world'}]
doc.list_field = list_value
self.assertEquals(doc._get_changed_fields(), ['list_field'])
self.assertEquals(doc._delta(), ({'list_field': list_value}, {}))
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
self.assertEquals(doc._get_changed_fields(), ['dict_field'])
self.assertEquals(doc._delta(), ({}, {'dict_field': 1}))
doc._changed_fields = []
doc.list_field = []
self.assertEquals(doc._get_changed_fields(), ['list_field'])
self.assertEquals(doc._delta(), ({}, {'list_field': 1}))
def test_delta_recursive(self):
"""Testing deltaing works with dynamic documents"""
class Embedded(DynamicEmbeddedDocument):
pass
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEquals(doc._get_changed_fields(), [])
self.assertEquals(doc._delta(), ({}, {}))
embedded_1 = Embedded()
embedded_1.string_field = 'hello'
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field = embedded_1
self.assertEquals(doc._get_changed_fields(), ['embedded_field'])
embedded_delta = {
'string_field': 'hello',
'int_field': 1,
'dict_field': {'hello': 'world'},
'list_field': ['1', 2, {'hello': 'world'}]
}
self.assertEquals(doc.embedded_field._delta(), (embedded_delta, {}))
embedded_delta.update({
'_types': ['Embedded'],
'_cls': 'Embedded',
})
self.assertEquals(doc._delta(), ({'embedded_field': embedded_delta}, {}))
doc.save()
doc.reload()
doc.embedded_field.dict_field = {}
self.assertEquals(doc._get_changed_fields(), ['embedded_field.dict_field'])
self.assertEquals(doc.embedded_field._delta(), ({}, {'dict_field': 1}))
self.assertEquals(doc._delta(), ({}, {'embedded_field.dict_field': 1}))
doc.save()
doc.reload()
doc.embedded_field.list_field = []
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
self.assertEquals(doc.embedded_field._delta(), ({}, {'list_field': 1}))
self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field': 1}))
doc.save()
doc.reload()
embedded_2 = Embedded()
embedded_2.string_field = 'hello'
embedded_2.int_field = 1
embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field.list_field = ['1', 2, embedded_2]
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
self.assertEquals(doc.embedded_field._delta(), ({
'list_field': ['1', 2, {
'_cls': 'Embedded',
'_types': ['Embedded'],
'string_field': 'hello',
'dict_field': {'hello': 'world'},
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
self.assertEquals(doc._delta(), ({
'embedded_field.list_field': ['1', 2, {
'_cls': 'Embedded',
'_types': ['Embedded'],
'string_field': 'hello',
'dict_field': {'hello': 'world'},
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
doc.save()
doc.reload()
self.assertEquals(doc.embedded_field.list_field[2]._changed_fields, [])
self.assertEquals(doc.embedded_field.list_field[0], '1')
self.assertEquals(doc.embedded_field.list_field[1], 2)
for k in doc.embedded_field.list_field[2]._fields:
self.assertEquals(doc.embedded_field.list_field[2][k], embedded_2[k])
doc.embedded_field.list_field[2].string_field = 'world'
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field.2.string_field'])
self.assertEquals(doc.embedded_field._delta(), ({'list_field.2.string_field': 'world'}, {}))
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.string_field': 'world'}, {}))
doc.save()
doc.reload()
self.assertEquals(doc.embedded_field.list_field[2].string_field, 'world')
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = 'hello world'
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
self.assertEquals(doc.embedded_field._delta(), ({
'list_field': ['1', 2, {
'_types': ['Embedded'],
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}]}, {}))
self.assertEquals(doc._delta(), ({
'embedded_field.list_field': ['1', 2, {
'_types': ['Embedded'],
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}
]}, {}))
doc.save()
doc.reload()
self.assertEquals(doc.embedded_field.list_field[2].string_field, 'hello world')
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}]}, {}))
doc.save()
doc.reload()
doc.embedded_field.list_field[2].list_field.append(1)
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}, 1]}, {}))
doc.save()
doc.reload()
self.assertEquals(doc.embedded_field.list_field[2].list_field, [2, {'hello': 'world'}, 1])
doc.embedded_field.list_field[2].list_field.sort()
doc.save()
doc.reload()
self.assertEquals(doc.embedded_field.list_field[2].list_field, [1, 2, {'hello': 'world'}])
del(doc.embedded_field.list_field[2].list_field[2]['hello'])
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [1, 2, {}]}, {}))
doc.save()
doc.reload()
del(doc.embedded_field.list_field[2].list_field)
self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field.2.list_field': 1}))
doc.save()
doc.reload()
doc.dict_field = {'embedded': embedded_1}
doc.save()
doc.reload()
doc.dict_field['embedded'].string_field = 'Hello World'
self.assertEquals(doc._get_changed_fields(), ['dict_field.embedded.string_field'])
self.assertEquals(doc._delta(), ({'dict_field.embedded.string_field': 'Hello World'}, {}))
def test_indexes(self):
"""Ensure that indexes are used when meta[indexes] is specified.
"""
class BlogPost(DynamicDocument):
meta = {
'indexes': [
'-date',
('category', '-date')
],
}
BlogPost.drop_collection()
info = BlogPost.objects._collection.index_information()
# _id, '-date', ('cat', 'date')
# NB: there is no index on _types by itself, since
# the indices on -date and tags will both contain
# _types as first element in the key
self.assertEqual(len(info), 3)
# Indexes are lazy so use list() to perform query
list(BlogPost.objects)
info = BlogPost.objects._collection.index_information()
info = [value['key'] for key, value in info.iteritems()]
self.assertTrue([('_types', 1), ('category', 1), ('date', -1)]
in info)
self.assertTrue([('_types', 1), ('date', -1)] in info)

View File

@@ -1,20 +1,29 @@
import unittest
import datetime
from decimal import Decimal
import pymongo
import os
import unittest
import uuid
import StringIO
import tempfile
import gridfs
from decimal import Decimal
from mongoengine import *
from mongoengine.connection import _get_db
from mongoengine.connection import get_db
from mongoengine.base import _document_registry, NotRegistered
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png')
class FieldTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = _get_db()
self.db = get_db()
def tearDown(self):
self.db.drop_collection('fs.files')
self.db.drop_collection('fs.chunks')
def test_default_values(self):
"""Ensure that default field values are used when creating a document.
@@ -44,6 +53,93 @@ class FieldTest(unittest.TestCase):
person = Person(age=30)
self.assertRaises(ValidationError, person.validate)
def test_not_required_handles_none_in_update(self):
"""Ensure that every fields should accept None if required is False.
"""
class HandleNoneFields(Document):
str_fld = StringField()
int_fld = IntField()
flt_fld = FloatField()
comp_dt_fld = ComplexDateTimeField()
HandleNoneFields.drop_collection()
doc = HandleNoneFields()
doc.str_fld = u'spam ham egg'
doc.int_fld = 42
doc.flt_fld = 4.2
doc.com_dt_fld = datetime.datetime.utcnow()
doc.save()
res = HandleNoneFields.objects(id=doc.id).update(
set__str_fld=None,
set__int_fld=None,
set__flt_fld=None,
set__comp_dt_fld=None,
)
self.assertEqual(res, 1)
# Retrive data from db and verify it.
ret = HandleNoneFields.objects.all()[0]
self.assertEqual(ret.str_fld, None)
self.assertEqual(ret.int_fld, None)
self.assertEqual(ret.flt_fld, None)
# Return current time if retrived value is None.
self.assertTrue(isinstance(ret.comp_dt_fld, datetime.datetime))
def test_not_required_handles_none_from_database(self):
"""Ensure that every fields can handle null values from the database.
"""
class HandleNoneFields(Document):
str_fld = StringField(required=True)
int_fld = IntField(required=True)
flt_fld = FloatField(required=True)
comp_dt_fld = ComplexDateTimeField(required=True)
HandleNoneFields.drop_collection()
doc = HandleNoneFields()
doc.str_fld = u'spam ham egg'
doc.int_fld = 42
doc.flt_fld = 4.2
doc.com_dt_fld = datetime.datetime.utcnow()
doc.save()
collection = self.db[HandleNoneFields._get_collection_name()]
obj = collection.update({"_id": doc.id}, {"$unset": {
"str_fld": 1,
"int_fld": 1,
"flt_fld": 1,
"comp_dt_fld": 1}
})
# Retrive data from db and verify it.
ret = HandleNoneFields.objects.all()[0]
self.assertEqual(ret.str_fld, None)
self.assertEqual(ret.int_fld, None)
self.assertEqual(ret.flt_fld, None)
# Return current time if retrived value is None.
self.assert_(isinstance(ret.comp_dt_fld, datetime.datetime))
self.assertRaises(ValidationError, ret.validate)
def test_int_and_float_ne_operator(self):
class TestDocument(Document):
int_fld = IntField()
float_fld = FloatField()
TestDocument.drop_collection()
TestDocument(int_fld=None, float_fld=None).save()
TestDocument(int_fld=1, float_fld=1).save()
self.assertEqual(1, TestDocument.objects(int_fld__ne=None).count())
self.assertEqual(1, TestDocument.objects(float_fld__ne=None).count())
def test_object_id_validation(self):
"""Ensure that invalid values cannot be assigned to string fields.
"""
@@ -175,6 +271,26 @@ class FieldTest(unittest.TestCase):
person.admin = 'Yes'
self.assertRaises(ValidationError, person.validate)
def test_uuid_validation(self):
"""Ensure that invalid values cannot be assigned to UUID fields.
"""
class Person(Document):
api_key = UUIDField()
person = Person()
# any uuid type is valid
person.api_key = uuid.uuid4()
person.validate()
person.api_key = uuid.uuid1()
person.validate()
# last g cannot belong to an hex number
person.api_key = '9d159858-549b-4975-9f98-dd2f987c113g'
self.assertRaises(ValidationError, person.validate)
# short strings don't validate
person.api_key = '9d159858-549b-4975-9f98-dd2f987c113'
self.assertRaises(ValidationError, person.validate)
def test_datetime_validation(self):
"""Ensure that invalid values cannot be assigned to datetime fields.
"""
@@ -242,24 +358,6 @@ class FieldTest(unittest.TestCase):
self.assertNotEquals(log.date, d1)
self.assertEquals(log.date, d2)
# Pre UTC microseconds above 1000 is wonky.
# log.date has an invalid microsecond value so I can't construct
# a date to compare.
#
# However, the timedelta is predicable with pre UTC timestamps
# It always adds 16 seconds and [777216-776217] microseconds
for i in xrange(1001, 3113, 33):
d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, i)
log.date = d1
log.save()
log.reload()
self.assertNotEquals(log.date, d1)
delta = log.date - d1
self.assertEquals(delta.seconds, 16)
microseconds = 777216 - (i % 1000)
self.assertEquals(delta.microseconds, microseconds)
LogEntry.drop_collection()
def test_complexdatetime_storage(self):
@@ -337,27 +435,27 @@ class FieldTest(unittest.TestCase):
logs = LogEntry.objects.order_by("date")
count = logs.count()
i = 0
while i == count-1:
self.assertTrue(logs[i].date <= logs[i+1].date)
i +=1
while i == count - 1:
self.assertTrue(logs[i].date <= logs[i + 1].date)
i += 1
logs = LogEntry.objects.order_by("-date")
count = logs.count()
i = 0
while i == count-1:
self.assertTrue(logs[i].date >= logs[i+1].date)
i +=1
while i == count - 1:
self.assertTrue(logs[i].date >= logs[i + 1].date)
i += 1
# Test searching
logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980,1,1))
logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1))
self.assertEqual(logs.count(), 30)
logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980,1,1))
logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1))
self.assertEqual(logs.count(), 30)
logs = LogEntry.objects.filter(
date__lte=datetime.datetime(2011,1,1),
date__gte=datetime.datetime(2000,1,1),
date__lte=datetime.datetime(2011, 1, 1),
date__gte=datetime.datetime(2000, 1, 1),
)
self.assertEqual(logs.count(), 10)
@@ -459,6 +557,31 @@ class FieldTest(unittest.TestCase):
BlogPost.drop_collection()
def test_reverse_list_sorting(self):
'''Ensure that a reverse sorted list field properly sorts values'''
class Category(EmbeddedDocument):
count = IntField()
name = StringField()
class CategoryList(Document):
categories = SortedListField(EmbeddedDocumentField(Category), ordering='count', reverse=True)
name = StringField()
catlist = CategoryList(name="Top categories")
cat1 = Category(name='posts', count=10)
cat2 = Category(name='food', count=100)
cat3 = Category(name='drink', count=40)
catlist.categories = [cat1, cat2, cat3]
catlist.save()
catlist.reload()
self.assertEqual(catlist.categories[0].name, cat2.name)
self.assertEqual(catlist.categories[1].name, cat3.name)
self.assertEqual(catlist.categories[2].name, cat1.name)
CategoryList.drop_collection()
def test_list_field(self):
"""Ensure that list types work as expected.
"""
@@ -485,7 +608,6 @@ class FieldTest(unittest.TestCase):
post.info = [{'test': 3}]
post.save()
self.assertEquals(BlogPost.objects.count(), 3)
self.assertEquals(BlogPost.objects.filter(info__exact='test').count(), 1)
self.assertEquals(BlogPost.objects.filter(info__0__test='test').count(), 1)
@@ -495,6 +617,21 @@ class FieldTest(unittest.TestCase):
self.assertEquals(BlogPost.objects.filter(info__100__test__exact='test').count(), 0)
BlogPost.drop_collection()
def test_list_field_passed_in_value(self):
class Foo(Document):
bars = ListField(ReferenceField("Bar"))
class Bar(Document):
text = StringField()
bar = Bar(text="hi")
bar.save()
foo = Foo(bars=[])
foo.bars.append(bar)
self.assertEquals(repr(foo.bars), '[<Bar: Bar object>]')
def test_list_field_strict(self):
"""Ensure that list field handles validation if provided a strict field type."""
@@ -515,6 +652,39 @@ class FieldTest(unittest.TestCase):
Simple.drop_collection()
def test_list_field_rejects_strings(self):
"""Strings aren't valid list field data types"""
class Simple(Document):
mapping = ListField()
Simple.drop_collection()
e = Simple()
e.mapping = 'hello world'
self.assertRaises(ValidationError, e.save)
def test_complex_field_required(self):
"""Ensure required cant be None / Empty"""
class Simple(Document):
mapping = ListField(required=True)
Simple.drop_collection()
e = Simple()
e.mapping = []
self.assertRaises(ValidationError, e.save)
class Simple(Document):
mapping = DictField(required=True)
Simple.drop_collection()
e = Simple()
e.mapping = {}
self.assertRaises(ValidationError, e.save)
def test_list_field_complex(self):
"""Ensure that the list fields can handle the complex types."""
@@ -582,6 +752,9 @@ class FieldTest(unittest.TestCase):
post.info = {'the.title': 'test'}
self.assertRaises(ValidationError, post.validate)
post.info = {1: 'test'}
self.assertRaises(ValidationError, post.validate)
post.info = {'title': 'test'}
post.save()
@@ -600,6 +773,13 @@ class FieldTest(unittest.TestCase):
# Confirm handles non strings or non existing keys
self.assertEquals(BlogPost.objects.filter(info__details__test__exact=5).count(), 0)
self.assertEquals(BlogPost.objects.filter(info__made_up__test__exact='test').count(), 0)
post = BlogPost.objects.create(info={'title': 'original'})
post.info.update({'title': 'updated'})
post.save()
post.reload()
self.assertEquals('updated', post.info['title'])
BlogPost.drop_collection()
def test_dictfield_strict(self):
@@ -727,6 +907,48 @@ class FieldTest(unittest.TestCase):
Extensible.drop_collection()
def test_embedded_mapfield_db_field(self):
class Embedded(EmbeddedDocument):
number = IntField(default=0, db_field='i')
class Test(Document):
my_map = MapField(field=EmbeddedDocumentField(Embedded), db_field='x')
Test.drop_collection()
test = Test()
test.my_map['DICTIONARY_KEY'] = Embedded(number=1)
test.save()
Test.objects.update_one(inc__my_map__DICTIONARY_KEY__number=1)
test = Test.objects.get()
self.assertEqual(test.my_map['DICTIONARY_KEY'].number, 2)
doc = self.db.test.find_one()
self.assertEqual(doc['x']['DICTIONARY_KEY']['i'], 2)
def test_embedded_db_field(self):
class Embedded(EmbeddedDocument):
number = IntField(default=0, db_field='i')
class Test(Document):
embedded = EmbeddedDocumentField(Embedded, db_field='x')
Test.drop_collection()
test = Test()
test.embedded = Embedded(number=1)
test.save()
Test.objects.update_one(inc__embedded__number=1)
test = Test.objects.get()
self.assertEqual(test.embedded.number, 2)
doc = self.db.test.find_one()
self.assertEqual(doc['x']['i'], 2)
def test_embedded_document_validation(self):
"""Ensure that invalid embedded documents cannot be assigned to
embedded document fields.
@@ -942,15 +1164,29 @@ class FieldTest(unittest.TestCase):
class Company(Document):
name = StringField()
Product.drop_collection()
Company.drop_collection()
ten_gen = Company(name='10gen')
ten_gen.save()
mongodb = Product(name='MongoDB', company=ten_gen)
mongodb.save()
me = Product(name='MongoEngine')
me.save()
obj = Product.objects(company=ten_gen).first()
self.assertEqual(obj, mongodb)
self.assertEqual(obj.company, ten_gen)
obj = Product.objects(company=None).first()
self.assertEqual(obj, me)
obj, created = Product.objects.get_or_create(company=None)
self.assertEqual(created, False)
self.assertEqual(obj, me)
def test_reference_query_conversion(self):
"""Ensure that ReferenceFields can be queried using objects and values
of the type of the primary key of the referenced object.
@@ -1062,7 +1298,6 @@ class FieldTest(unittest.TestCase):
Post.drop_collection()
User.drop_collection()
def test_generic_reference_document_not_registered(self):
"""Ensure dereferencing out of the document registry throws a
`NotRegistered` error.
@@ -1089,7 +1324,7 @@ class FieldTest(unittest.TestCase):
user = User.objects.first()
try:
user.bookmarks
raise AssertionError, "Link was removed from the registry"
raise AssertionError("Link was removed from the registry")
except NotRegistered:
pass
@@ -1108,6 +1343,74 @@ class FieldTest(unittest.TestCase):
self.assertEquals(repr(Person.objects(city=None)),
"[<Person: Person object>]")
def test_generic_reference_choices(self):
"""Ensure that a GenericReferenceField can handle choices
"""
class Link(Document):
title = StringField()
class Post(Document):
title = StringField()
class Bookmark(Document):
bookmark_object = GenericReferenceField(choices=(Post,))
Link.drop_collection()
Post.drop_collection()
Bookmark.drop_collection()
link_1 = Link(title="Pitchfork")
link_1.save()
post_1 = Post(title="Behind the Scenes of the Pavement Reunion")
post_1.save()
bm = Bookmark(bookmark_object=link_1)
self.assertRaises(ValidationError, bm.validate)
bm = Bookmark(bookmark_object=post_1)
bm.save()
bm = Bookmark.objects.first()
self.assertEqual(bm.bookmark_object, post_1)
def test_generic_reference_list_choices(self):
"""Ensure that a ListField properly dereferences generic references and
respects choices.
"""
class Link(Document):
title = StringField()
class Post(Document):
title = StringField()
class User(Document):
bookmarks = ListField(GenericReferenceField(choices=(Post,)))
Link.drop_collection()
Post.drop_collection()
User.drop_collection()
link_1 = Link(title="Pitchfork")
link_1.save()
post_1 = Post(title="Behind the Scenes of the Pavement Reunion")
post_1.save()
user = User(bookmarks=[link_1])
self.assertRaises(ValidationError, user.validate)
user = User(bookmarks=[post_1])
user.save()
user = User.objects.first()
self.assertEqual(user.bookmarks, [post_1])
Link.drop_collection()
Post.drop_collection()
User.drop_collection()
def test_binary_fields(self):
"""Ensure that binary fields can be stored and retrieved.
"""
@@ -1213,6 +1516,53 @@ class FieldTest(unittest.TestCase):
Shirt.drop_collection()
def test_simple_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_simple_choices_get_field_display(self):
"""Test dynamic helper for returning the display value of a choices field.
"""
class Shirt(Document):
size = StringField(max_length=3, choices=('S', 'M', 'L', 'XL', 'XXL'))
style = StringField(max_length=3, choices=('Small', 'Baggy', 'wide'), default='Small')
Shirt.drop_collection()
shirt = Shirt()
self.assertEqual(shirt.get_size_display(), None)
self.assertEqual(shirt.get_style_display(), 'Small')
shirt.size = "XXL"
shirt.style = "Baggy"
self.assertEqual(shirt.get_size_display(), 'XXL')
self.assertEqual(shirt.get_style_display(), 'Baggy')
# Set as Z - an invalid choice
shirt.size = "Z"
shirt.style = "Z"
self.assertEqual(shirt.get_size_display(), 'Z')
self.assertEqual(shirt.get_style_display(), 'Z')
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
"""
@@ -1242,6 +1592,21 @@ class FieldTest(unittest.TestCase):
self.assertEquals(result.file.read(), text)
self.assertEquals(result.file.content_type, content_type)
result.file.delete() # Remove file from GridFS
PutFile.objects.delete()
# Ensure file-like objects are stored
putfile = PutFile()
putstring = StringIO.StringIO()
putstring.write(text)
putstring.seek(0)
putfile.file.put(putstring, 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()
streamfile = StreamFile()
streamfile.file.new_file(content_type=content_type)
@@ -1289,7 +1654,50 @@ class FieldTest(unittest.TestCase):
# Make sure FileField is optional and not required
class DemoFile(Document):
file = FileField()
d = DemoFile.objects.create()
DemoFile.objects.create()
def test_file_field_no_default(self):
class GridDocument(Document):
the_file = FileField()
GridDocument.drop_collection()
with tempfile.TemporaryFile() as f:
f.write("Hello World!")
f.flush()
# Test without default
doc_a = GridDocument()
doc_a.save()
doc_b = GridDocument.objects.with_id(doc_a.id)
doc_b.the_file.replace(f, filename='doc_b')
doc_b.save()
self.assertNotEquals(doc_b.the_file.grid_id, None)
# Test it matches
doc_c = GridDocument.objects.with_id(doc_b.id)
self.assertEquals(doc_b.the_file.grid_id, doc_c.the_file.grid_id)
# Test with default
doc_d = GridDocument(the_file='')
doc_d.save()
doc_e = GridDocument.objects.with_id(doc_d.id)
self.assertEquals(doc_d.the_file.grid_id, doc_e.the_file.grid_id)
doc_e.the_file.replace(f, filename='doc_e')
doc_e.save()
doc_f = GridDocument.objects.with_id(doc_e.id)
self.assertEquals(doc_e.the_file.grid_id, doc_f.the_file.grid_id)
db = GridDocument._get_db()
grid_fs = gridfs.GridFS(db)
self.assertEquals(['doc_b', 'doc_e'], grid_fs.list())
def test_file_uniqueness(self):
"""Ensure that each instance of a FileField is unique
@@ -1328,6 +1736,95 @@ class FieldTest(unittest.TestCase):
TestFile.drop_collection()
def test_image_field(self):
class TestImage(Document):
image = ImageField()
TestImage.drop_collection()
t = TestImage()
t.image.put(open(TEST_IMAGE_PATH, 'r'))
t.save()
t = TestImage.objects.first()
self.assertEquals(t.image.format, 'PNG')
w, h = t.image.size
self.assertEquals(w, 371)
self.assertEquals(h, 76)
t.image.delete()
def test_image_field_resize(self):
class TestImage(Document):
image = ImageField(size=(185, 37))
TestImage.drop_collection()
t = TestImage()
t.image.put(open(TEST_IMAGE_PATH, 'r'))
t.save()
t = TestImage.objects.first()
self.assertEquals(t.image.format, 'PNG')
w, h = t.image.size
self.assertEquals(w, 185)
self.assertEquals(h, 37)
t.image.delete()
def test_image_field_thumbnail(self):
class TestImage(Document):
image = ImageField(thumbnail_size=(92, 18))
TestImage.drop_collection()
t = TestImage()
t.image.put(open(TEST_IMAGE_PATH, 'r'))
t.save()
t = TestImage.objects.first()
self.assertEquals(t.image.thumbnail.format, 'PNG')
self.assertEquals(t.image.thumbnail.width, 92)
self.assertEquals(t.image.thumbnail.height, 18)
t.image.delete()
def test_file_multidb(self):
register_connection('testfiles', 'testfiles')
class TestFile(Document):
name = StringField()
file = FileField(db_alias="testfiles",
collection_name="macumba")
TestFile.drop_collection()
# delete old filesystem
get_db("testfiles").macumba.files.drop()
get_db("testfiles").macumba.chunks.drop()
# First instance
testfile = TestFile()
testfile.name = "Hello, World!"
testfile.file.put('Hello, World!',
name="hello.txt")
testfile.save()
data = get_db("testfiles").macumba.files.find_one()
self.assertEquals(data.get('name'), 'hello.txt')
testfile = TestFile.objects.first()
self.assertEquals(testfile.file.read(),
'Hello, World!')
def test_geo_indexes(self):
"""Ensure that indexes are created automatically for GeoPointFields.
"""
@@ -1488,7 +1985,6 @@ class FieldTest(unittest.TestCase):
c = self.db['mongoengine.counters'].find_one({'_id': 'animal.id'})
self.assertEqual(c['next'], 10)
def test_generic_embedded_document(self):
class Car(EmbeddedDocument):
name = StringField()
@@ -1501,6 +1997,8 @@ class FieldTest(unittest.TestCase):
name = StringField()
like = GenericEmbeddedDocumentField()
Person.drop_collection()
person = Person(name='Test User')
person.like = Car(name='Fiat')
person.save()
@@ -1514,5 +2012,103 @@ class FieldTest(unittest.TestCase):
person = Person.objects.first()
self.assertTrue(isinstance(person.like, Dish))
def test_generic_embedded_document_choices(self):
"""Ensure you can limit GenericEmbeddedDocument choices
"""
class Car(EmbeddedDocument):
name = StringField()
class Dish(EmbeddedDocument):
food = StringField(required=True)
number = IntField()
class Person(Document):
name = StringField()
like = GenericEmbeddedDocumentField(choices=(Dish,))
Person.drop_collection()
person = Person(name='Test User')
person.like = Car(name='Fiat')
self.assertRaises(ValidationError, person.validate)
person.like = Dish(food="arroz", number=15)
person.save()
person = Person.objects.first()
self.assertTrue(isinstance(person.like, Dish))
def test_generic_list_embedded_document_choices(self):
"""Ensure you can limit GenericEmbeddedDocument choices inside a list
field
"""
class Car(EmbeddedDocument):
name = StringField()
class Dish(EmbeddedDocument):
food = StringField(required=True)
number = IntField()
class Person(Document):
name = StringField()
likes = ListField(GenericEmbeddedDocumentField(choices=(Dish,)))
Person.drop_collection()
person = Person(name='Test User')
person.likes = [Car(name='Fiat')]
self.assertRaises(ValidationError, person.validate)
person.likes = [Dish(food="arroz", number=15)]
person.save()
person = Person.objects.first()
self.assertTrue(isinstance(person.likes[0], Dish))
def test_recursive_validation(self):
"""Ensure that a validation result to_dict is available.
"""
class Author(EmbeddedDocument):
name = StringField(required=True)
class Comment(EmbeddedDocument):
author = EmbeddedDocumentField(Author, required=True)
content = StringField(required=True)
class Post(Document):
title = StringField(required=True)
comments = ListField(EmbeddedDocumentField(Comment))
bob = Author(name='Bob')
post = Post(title='hello world')
post.comments.append(Comment(content='hello', author=bob))
post.comments.append(Comment(author=bob))
try:
post.validate()
except ValidationError, error:
pass
# ValidationError.errors property
self.assertTrue(hasattr(error, 'errors'))
self.assertTrue(isinstance(error.errors, dict))
self.assertTrue('comments' in error.errors)
self.assertTrue(1 in error.errors['comments'])
self.assertTrue(isinstance(error.errors['comments'][1]['content'],
ValidationError))
# ValidationError.schema property
error_dict = error.to_dict()
self.assertTrue(isinstance(error_dict, dict))
self.assertTrue('comments' in error_dict)
self.assertTrue(1 in error_dict['comments'])
self.assertTrue('content' in error_dict['comments'][1])
self.assertEquals(error_dict['comments'][1]['content'],
'Field is required')
post.comments[1].content = 'here we go'
post.validate()
if __name__ == '__main__':
unittest.main()

View File

@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
import unittest
import pymongo
from bson import ObjectId
from datetime import datetime, timedelta
from mongoengine.queryset import (QuerySet, QuerySetManager,
MultipleObjectsReturned, DoesNotExist,
QueryFieldList)
from mongoengine import *
from mongoengine.connection import _get_connection
from mongoengine.connection import get_connection
from mongoengine.tests import query_counter
@@ -15,10 +16,11 @@ class QuerySetTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
class Person(Document):
name = StringField()
age = IntField()
meta = {'allow_inheritance': True}
self.Person = Person
def test_initialisation(self):
@@ -59,8 +61,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(len(people), 2)
results = list(people)
self.assertTrue(isinstance(results[0], self.Person))
self.assertTrue(isinstance(results[0].id, (pymongo.objectid.ObjectId,
str, unicode)))
self.assertTrue(isinstance(results[0].id, (ObjectId, str, unicode)))
self.assertEqual(results[0].name, "User A")
self.assertEqual(results[0].age, 20)
self.assertEqual(results[1].name, "User B")
@@ -110,6 +111,16 @@ class QuerySetTest(unittest.TestCase):
people = list(self.Person.objects[80000:80001])
self.assertEqual(len(people), 0)
# Test larger slice __repr__
self.Person.objects.delete()
for i in xrange(55):
self.Person(name='A%s' % i, age=i).save()
self.assertEqual(len(self.Person.objects), 55)
self.assertEqual("Person object", "%s" % self.Person.objects[0])
self.assertEqual("[<Person: Person object>, <Person: Person object>]", "%s" % self.Person.objects[1:3])
self.assertEqual("[<Person: Person object>, <Person: Person object>]", "%s" % self.Person.objects[51:53])
def test_find_one(self):
"""Ensure that a query using find_one returns a valid result.
"""
@@ -144,6 +155,8 @@ class QuerySetTest(unittest.TestCase):
person = self.Person.objects.with_id(person1.id)
self.assertEqual(person.name, "User A")
self.assertRaises(InvalidQueryError, self.Person.objects(name="User A").with_id, person1.id)
def test_find_only_one(self):
"""Ensure that a query using ``get`` returns at most one result.
"""
@@ -316,11 +329,11 @@ class QuerySetTest(unittest.TestCase):
BlogPost(title="ABC", comments=[c1, c2]).save()
BlogPost.objects(comments__by="joe").update(inc__comments__S__votes=1)
BlogPost.objects(comments__by="jane").update(inc__comments__S__votes=1)
post = BlogPost.objects.first()
self.assertEquals(post.comments[0].by, 'joe')
self.assertEquals(post.comments[0].votes, 4)
self.assertEquals(post.comments[1].by, 'jane')
self.assertEquals(post.comments[1].votes, 8)
# Currently the $ operator only applies to the first matched item in
# the query
@@ -368,6 +381,34 @@ class QuerySetTest(unittest.TestCase):
self.assertRaises(OperationError, update_nested)
Simple.drop_collection()
def test_update_using_positional_operator_embedded_document(self):
"""Ensure that the embedded documents can be updated using the positional
operator."""
class Vote(EmbeddedDocument):
score = IntField()
class Comment(EmbeddedDocument):
by = StringField()
votes = EmbeddedDocumentField(Vote)
class BlogPost(Document):
title = StringField()
comments = ListField(EmbeddedDocumentField(Comment))
BlogPost.drop_collection()
c1 = Comment(by="joe", votes=Vote(score=3))
c2 = Comment(by="jane", votes=Vote(score=7))
BlogPost(title="ABC", comments=[c1, c2]).save()
BlogPost.objects(comments__by="joe").update(set__comments__S__votes=Vote(score=4))
post = BlogPost.objects.first()
self.assertEquals(post.comments[0].by, 'joe')
self.assertEquals(post.comments[0].votes.score, 4)
def test_mapfield_update(self):
"""Ensure that the MapField can be updated."""
class Member(EmbeddedDocument):
@@ -439,7 +480,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(person.name, "User C")
def test_bulk_insert(self):
"""Ensure that query by array position works.
"""Ensure that bulk insert works
"""
class Comment(EmbeddedDocument):
@@ -449,12 +490,15 @@ class QuerySetTest(unittest.TestCase):
comments = ListField(EmbeddedDocumentField(Comment))
class Blog(Document):
title = StringField()
title = StringField(unique=True)
tags = ListField(StringField())
posts = ListField(EmbeddedDocumentField(Post))
Blog.drop_collection()
# Recreates the collection
self.assertEqual(0, Blog.objects.count())
with query_counter() as q:
self.assertEqual(q, 0)
@@ -468,10 +512,10 @@ class QuerySetTest(unittest.TestCase):
blogs.append(Blog(title="post %s" % i, posts=[post1, post2]))
Blog.objects.insert(blogs, load_bulk=False)
self.assertEqual(q, 2) # 1 for the inital connection and 1 for the insert
self.assertEqual(q, 1) # 1 for the insert
Blog.objects.insert(blogs)
self.assertEqual(q, 4) # 1 for insert, and 1 for in bulk
self.assertEqual(q, 3) # 1 for insert, and 1 for in bulk fetch (3 in total)
Blog.drop_collection()
@@ -519,6 +563,23 @@ class QuerySetTest(unittest.TestCase):
obj_id = Blog.objects.insert(blog1, load_bulk=False)
self.assertEquals(obj_id.__class__.__name__, 'ObjectId')
Blog.drop_collection()
post3 = Post(comments=[comment1, comment1])
blog1 = Blog(title="foo", posts=[post1, post2])
blog2 = Blog(title="bar", posts=[post2, post3])
blog3 = Blog(title="baz", posts=[post1, post2])
Blog.objects.insert([blog1, blog2])
def throw_operation_error_not_unique():
Blog.objects.insert([blog2, blog3], safe=True)
self.assertRaises(OperationError, throw_operation_error_not_unique)
self.assertEqual(Blog.objects.count(), 2)
Blog.objects.insert([blog2, blog3], write_options={'continue_on_error': True})
self.assertEqual(Blog.objects.count(), 3)
def test_slave_okay(self):
"""Ensures that a query can take slave_okay syntax
"""
@@ -567,19 +628,46 @@ class QuerySetTest(unittest.TestCase):
people1 = [person for person in queryset]
people2 = [person for person in queryset]
self.assertEqual(people1, people2)
def test_repr_iteration(self):
"""Ensure that QuerySet __repr__ can handle loops
"""
self.Person(name='Person 1').save()
self.Person(name='Person 2').save()
queryset = self.Person.objects
self.assertEquals('[<Person: Person object>, <Person: Person object>]', repr(queryset))
# Check that it still works even if iteration is interrupted.
for person in queryset:
self.assertEquals('.. queryset mid-iteration ..', repr(queryset))
break
people3 = [person for person in queryset]
self.assertEqual(people1, people2)
self.assertEqual(people1, people3)
def test_repr(self):
"""Test repr behavior isnt destructive"""
class Doc(Document):
number = IntField()
def __repr__(self):
return "<Doc: %s>" % self.number
Doc.drop_collection()
for i in xrange(1000):
Doc(number=i).save()
docs = Doc.objects.order_by('number')
self.assertEquals(docs.count(), 1000)
self.assertEquals(len(docs), 1000)
docs_string = "%s" % docs
self.assertTrue("Doc: 0" in docs_string)
self.assertEquals(docs.count(), 1000)
self.assertEquals(len(docs), 1000)
# Limit and skip
self.assertEquals('[<Doc: 1>, <Doc: 2>, <Doc: 3>]', "%s" % docs[1:4])
self.assertEquals(docs.count(), 3)
self.assertEquals(len(docs), 3)
for doc in docs:
self.assertEqual('.. queryset mid-iteration ..', repr(docs))
def test_regex_query_shortcuts(self):
"""Ensure that contains, startswith, endswith, etc work.
@@ -1277,6 +1365,37 @@ class QuerySetTest(unittest.TestCase):
self.Person.objects(name='Test User').delete()
self.assertEqual(1, BlogPost.objects.count())
def test_reverse_delete_rule_cascade_self_referencing(self):
"""Ensure self-referencing CASCADE deletes do not result in infinite loop
"""
class Category(Document):
name = StringField()
parent = ReferenceField('self', reverse_delete_rule=CASCADE)
num_children = 3
base = Category(name='Root')
base.save()
# Create a simple parent-child tree
for i in range(num_children):
child_name = 'Child-%i' % i
child = Category(name=child_name, parent=base)
child.save()
for i in range(num_children):
child_child_name = 'Child-Child-%i' % i
child_child = Category(name=child_child_name, parent=child)
child_child.save()
tree_size = 1 + num_children + (num_children * num_children)
self.assertEquals(tree_size, Category.objects.count())
self.assertEquals(num_children, Category.objects(parent=base).count())
# The delete should effectively wipe out the Category collection
# without resulting in infinite parent-child cascade recursion
base.delete()
self.assertEquals(0, Category.objects.count())
def test_reverse_delete_rule_nullify(self):
"""Ensure nullification of references to deleted documents.
"""
@@ -1321,6 +1440,36 @@ class QuerySetTest(unittest.TestCase):
self.assertRaises(OperationError, self.Person.objects.delete)
def test_reverse_delete_rule_pull(self):
"""Ensure pulling of references to deleted documents.
"""
class BlogPost(Document):
content = StringField()
authors = ListField(ReferenceField(self.Person,
reverse_delete_rule=PULL))
BlogPost.drop_collection()
self.Person.drop_collection()
me = self.Person(name='Test User')
me.save()
someoneelse = self.Person(name='Some-one Else')
someoneelse.save()
post = BlogPost(content='Watching TV', authors=[me, someoneelse])
post.save()
another = BlogPost(content='Chilling Out', authors=[someoneelse])
another.save()
someoneelse.delete()
post.reload()
another.reload()
self.assertEqual(post.authors, [me])
self.assertEqual(another.authors, [])
def test_update(self):
"""Ensure that atomic updates work properly.
"""
@@ -1371,20 +1520,85 @@ class QuerySetTest(unittest.TestCase):
BlogPost.drop_collection()
def test_update_pull(self):
def test_update_push_and_pull_add_to_set(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'])
BlogPost.drop_collection()
post = BlogPost(slug="test")
post.save()
BlogPost.objects.filter(id=post.id).update(push__tags="code")
post.reload()
self.assertEqual(post.tags, ["code"])
BlogPost.objects.filter(id=post.id).update(push_all__tags=["mongodb", "code"])
post.reload()
self.assertEqual(post.tags, ["code", "mongodb", "code"])
BlogPost.objects(slug="test").update(pull__tags="code")
post.reload()
self.assertTrue('code' not in post.tags)
self.assertEqual(len(post.tags), 1)
self.assertEqual(post.tags, ["mongodb"])
BlogPost.objects(slug="test").update(pull_all__tags=["mongodb", "code"])
post.reload()
self.assertEqual(post.tags, [])
BlogPost.objects(slug="test").update(__raw__={"$addToSet": {"tags": {"$each": ["code", "mongodb", "code"]}}})
post.reload()
self.assertEqual(post.tags, ["code", "mongodb"])
def test_add_to_set_each(self):
class Item(Document):
name = StringField(required=True)
description = StringField(max_length=50)
parents = ListField(ReferenceField('self'))
Item.drop_collection()
item = Item(name='test item').save()
parent_1 = Item(name='parent 1').save()
parent_2 = Item(name='parent 2').save()
item.update(add_to_set__parents=[parent_1, parent_2, parent_1])
item.reload()
self.assertEqual([parent_1, parent_2], item.parents)
def test_pull_nested(self):
class User(Document):
name = StringField()
class Collaborator(EmbeddedDocument):
user = StringField()
def __unicode__(self):
return '%s' % self.user
class Site(Document):
name = StringField(max_length=75, unique=True, required=True)
collaborators = ListField(EmbeddedDocumentField(Collaborator))
Site.drop_collection()
c = Collaborator(user='Esteban')
s = Site(name="test", collaborators=[c])
s.save()
Site.objects(id=s.id).update_one(pull__collaborators__user='Esteban')
self.assertEqual(Site.objects.first().collaborators, [])
def pull_all():
Site.objects(id=s.id).update_one(pull_all__collaborators__user=['Ross'])
self.assertRaises(InvalidQueryError, pull_all)
def test_update_one_pop_generic_reference(self):
@@ -1449,6 +1663,37 @@ class QuerySetTest(unittest.TestCase):
BlogPost.drop_collection()
def test_set_list_embedded_documents(self):
class Author(EmbeddedDocument):
name = StringField()
class Message(Document):
title = StringField()
authors = ListField(EmbeddedDocumentField('Author'))
Message.drop_collection()
message = Message(title="hello", authors=[Author(name="Harry")])
message.save()
Message.objects(authors__name="Harry").update_one(
set__authors__S=Author(name="Ross"))
message = message.reload()
self.assertEquals(message.authors[0].name, "Ross")
Message.objects(authors__name="Ross").update_one(
set__authors=[Author(name="Harry"),
Author(name="Ross"),
Author(name="Adam")])
message = message.reload()
self.assertEquals(message.authors[0].name, "Harry")
self.assertEquals(message.authors[1].name, "Ross")
self.assertEquals(message.authors[2].name, "Adam")
def test_order_by(self):
"""Ensure that QuerySets may be ordered.
"""
@@ -1749,9 +1994,9 @@ class QuerySetTest(unittest.TestCase):
# Check item_frequencies works for non-list fields
def test_assertions(f):
self.assertEqual(set(['1', '2']), set(f.keys()))
self.assertEqual(f['1'], 1)
self.assertEqual(f['2'], 2)
self.assertEqual(set([1, 2]), set(f.keys()))
self.assertEqual(f[1], 1)
self.assertEqual(f[2], 2)
exec_js = BlogPost.objects.item_frequencies('hits')
map_reduce = BlogPost.objects.item_frequencies('hits', map_reduce=True)
@@ -1840,6 +2085,80 @@ class QuerySetTest(unittest.TestCase):
freq = Person.objects.item_frequencies('city', normalize=True, map_reduce=True)
self.assertEquals(freq, {'CRB': 0.5, None: 0.5})
def test_item_frequencies_with_null_embedded(self):
class Data(EmbeddedDocument):
name = StringField()
class Extra(EmbeddedDocument):
tag = StringField()
class Person(Document):
data = EmbeddedDocumentField(Data, required=True)
extra = EmbeddedDocumentField(Extra)
Person.drop_collection()
p = Person()
p.data = Data(name="Wilson Jr")
p.save()
p = Person()
p.data = Data(name="Wesley")
p.extra = Extra(tag="friend")
p.save()
ot = Person.objects.item_frequencies('extra.tag', map_reduce=False)
self.assertEquals(ot, {None: 1.0, u'friend': 1.0})
ot = Person.objects.item_frequencies('extra.tag', map_reduce=True)
self.assertEquals(ot, {None: 1.0, u'friend': 1.0})
def test_item_frequencies_with_0_values(self):
class Test(Document):
val = IntField()
Test.drop_collection()
t = Test()
t.val = 0
t.save()
ot = Test.objects.item_frequencies('val', map_reduce=True)
self.assertEquals(ot, {0: 1})
ot = Test.objects.item_frequencies('val', map_reduce=False)
self.assertEquals(ot, {0: 1})
def test_item_frequencies_with_False_values(self):
class Test(Document):
val = BooleanField()
Test.drop_collection()
t = Test()
t.val = False
t.save()
ot = Test.objects.item_frequencies('val', map_reduce=True)
self.assertEquals(ot, {False: 1})
ot = Test.objects.item_frequencies('val', map_reduce=False)
self.assertEquals(ot, {False: 1})
def test_item_frequencies_normalize(self):
class Test(Document):
val = IntField()
Test.drop_collection()
for i in xrange(50):
Test(val=1).save()
for i in xrange(20):
Test(val=2).save()
freqs = Test.objects.item_frequencies('val', map_reduce=False, normalize=True)
self.assertEquals(freqs, {1: 50.0/70, 2: 20.0/70})
freqs = Test.objects.item_frequencies('val', map_reduce=True, normalize=True)
self.assertEquals(freqs, {1: 50.0/70, 2: 20.0/70})
def test_average(self):
"""Ensure that field can be averaged correctly.
"""
@@ -1882,6 +2201,24 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(set(self.Person.objects(age=30).distinct('name')),
set(['Mr Orange', 'Mr Pink']))
def test_distinct_handles_references(self):
class Foo(Document):
bar = ReferenceField("Bar")
class Bar(Document):
text = StringField()
Bar.drop_collection()
Foo.drop_collection()
bar = Bar(text="hi")
bar.save()
foo = Foo(bar=bar)
foo.save()
self.assertEquals(Foo.objects.distinct("bar"), [bar])
def test_custom_manager(self):
"""Ensure that custom QuerySetManager instances work as expected.
"""
@@ -2197,10 +2534,10 @@ class QuerySetTest(unittest.TestCase):
events = Event.objects(location__within_box=box)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event2.id)
# check that polygon works for users who have a server >= 1.9
server_version = tuple(
_get_connection().server_info()['version'].split('.')
get_connection().server_info()['version'].split('.')
)
required_version = tuple("1.9.0".split("."))
if server_version >= required_version:
@@ -2214,7 +2551,7 @@ class QuerySetTest(unittest.TestCase):
events = Event.objects(location__within_polygon=polygon)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event1.id)
polygon2 = [
(54.033586,-1.742249),
(52.792797,-1.225891),
@@ -2222,7 +2559,7 @@ class QuerySetTest(unittest.TestCase):
]
events = Event.objects(location__within_polygon=polygon2)
self.assertEqual(events.count(), 0)
Event.drop_collection()
def test_spherical_geospatial_operators(self):
@@ -2569,6 +2906,278 @@ class QuerySetTest(unittest.TestCase):
self.assertRaises(TypeError, invalid_where)
def test_scalar(self):
class Organization(Document):
id = ObjectIdField('_id')
name = StringField()
class User(Document):
id = ObjectIdField('_id')
name = StringField()
organization = ObjectIdField()
User.drop_collection()
Organization.drop_collection()
whitehouse = Organization(name="White House")
whitehouse.save()
User(name="Bob Dole", organization=whitehouse.id).save()
# Efficient way to get all unique organization names for a given
# set of users (Pretend this has additional filtering.)
user_orgs = set(User.objects.scalar('organization'))
orgs = Organization.objects(id__in=user_orgs).scalar('name')
self.assertEqual(list(orgs), ['White House'])
# Efficient for generating listings, too.
orgs = Organization.objects.scalar('name').in_bulk(list(user_orgs))
user_map = User.objects.scalar('name', 'organization')
user_listing = [(user, orgs[org]) for user, org in user_map]
self.assertEqual([("Bob Dole", "White House")], user_listing)
def test_scalar_simple(self):
class TestDoc(Document):
x = IntField()
y = BooleanField()
TestDoc.drop_collection()
TestDoc(x=10, y=True).save()
TestDoc(x=20, y=False).save()
TestDoc(x=30, y=True).save()
plist = list(TestDoc.objects.scalar('x', 'y'))
self.assertEqual(len(plist), 3)
self.assertEqual(plist[0], (10, True))
self.assertEqual(plist[1], (20, False))
self.assertEqual(plist[2], (30, True))
class UserDoc(Document):
name = StringField()
age = IntField()
UserDoc.drop_collection()
UserDoc(name="Wilson Jr", age=19).save()
UserDoc(name="Wilson", age=43).save()
UserDoc(name="Eliana", age=37).save()
UserDoc(name="Tayza", age=15).save()
ulist = list(UserDoc.objects.scalar('name', 'age'))
self.assertEqual(ulist, [
(u'Wilson Jr', 19),
(u'Wilson', 43),
(u'Eliana', 37),
(u'Tayza', 15)])
ulist = list(UserDoc.objects.scalar('name').order_by('age'))
self.assertEqual(ulist, [
(u'Tayza'),
(u'Wilson Jr'),
(u'Eliana'),
(u'Wilson')])
def test_scalar_embedded(self):
class Profile(EmbeddedDocument):
name = StringField()
age = IntField()
class Locale(EmbeddedDocument):
city = StringField()
country = StringField()
class Person(Document):
profile = EmbeddedDocumentField(Profile)
locale = EmbeddedDocumentField(Locale)
Person.drop_collection()
Person(profile=Profile(name="Wilson Jr", age=19),
locale=Locale(city="Corumba-GO", country="Brazil")).save()
Person(profile=Profile(name="Gabriel Falcao", age=23),
locale=Locale(city="New York", country="USA")).save()
Person(profile=Profile(name="Lincoln de souza", age=28),
locale=Locale(city="Belo Horizonte", country="Brazil")).save()
Person(profile=Profile(name="Walter cruz", age=30),
locale=Locale(city="Brasilia", country="Brazil")).save()
self.assertEqual(
list(Person.objects.order_by('profile__age').scalar('profile__name')),
[u'Wilson Jr', u'Gabriel Falcao', u'Lincoln de souza', u'Walter cruz'])
ulist = list(Person.objects.order_by('locale.city')
.scalar('profile__name', 'profile__age', 'locale__city'))
self.assertEqual(ulist,
[(u'Lincoln de souza', 28, u'Belo Horizonte'),
(u'Walter cruz', 30, u'Brasilia'),
(u'Wilson Jr', 19, u'Corumba-GO'),
(u'Gabriel Falcao', 23, u'New York')])
def test_scalar_decimal(self):
from decimal import Decimal
class Person(Document):
name = StringField()
rating = DecimalField()
Person.drop_collection()
Person(name="Wilson Jr", rating=Decimal('1.0')).save()
ulist = list(Person.objects.scalar('name', 'rating'))
self.assertEqual(ulist, [(u'Wilson Jr', Decimal('1.0'))])
def test_scalar_reference_field(self):
class State(Document):
name = StringField()
class Person(Document):
name = StringField()
state = ReferenceField(State)
State.drop_collection()
Person.drop_collection()
s1 = State(name="Goias")
s1.save()
Person(name="Wilson JR", state=s1).save()
plist = list(Person.objects.scalar('name', 'state'))
self.assertEqual(plist, [(u'Wilson JR', s1)])
def test_scalar_generic_reference_field(self):
class State(Document):
name = StringField()
class Person(Document):
name = StringField()
state = GenericReferenceField()
State.drop_collection()
Person.drop_collection()
s1 = State(name="Goias")
s1.save()
Person(name="Wilson JR", state=s1).save()
plist = list(Person.objects.scalar('name', 'state'))
self.assertEqual(plist, [(u'Wilson JR', s1)])
def test_scalar_db_field(self):
class TestDoc(Document):
x = IntField()
y = BooleanField()
TestDoc.drop_collection()
TestDoc(x=10, y=True).save()
TestDoc(x=20, y=False).save()
TestDoc(x=30, y=True).save()
plist = list(TestDoc.objects.scalar('x', 'y'))
self.assertEqual(len(plist), 3)
self.assertEqual(plist[0], (10, True))
self.assertEqual(plist[1], (20, False))
self.assertEqual(plist[2], (30, True))
def test_scalar_primary_key(self):
class SettingValue(Document):
key = StringField(primary_key=True)
value = StringField()
SettingValue.drop_collection()
s = SettingValue(key="test", value="test value")
s.save()
val = SettingValue.objects.scalar('key', 'value')
self.assertEqual(list(val), [('test', 'test value')])
def test_scalar_cursor_behaviour(self):
"""Ensure that a query returns a valid set of results.
"""
person1 = self.Person(name="User A", age=20)
person1.save()
person2 = self.Person(name="User B", age=30)
person2.save()
# Find all people in the collection
people = self.Person.objects.scalar('name')
self.assertEqual(len(people), 2)
results = list(people)
self.assertEqual(results[0], "User A")
self.assertEqual(results[1], "User B")
# Use a query to filter the people found to just person1
people = self.Person.objects(age=20).scalar('name')
self.assertEqual(len(people), 1)
person = people.next()
self.assertEqual(person, "User A")
# Test limit
people = list(self.Person.objects.limit(1).scalar('name'))
self.assertEqual(len(people), 1)
self.assertEqual(people[0], 'User A')
# Test skip
people = list(self.Person.objects.skip(1).scalar('name'))
self.assertEqual(len(people), 1)
self.assertEqual(people[0], 'User B')
person3 = self.Person(name="User C", age=40)
person3.save()
# Test slice limit
people = list(self.Person.objects[:2].scalar('name'))
self.assertEqual(len(people), 2)
self.assertEqual(people[0], 'User A')
self.assertEqual(people[1], 'User B')
# Test slice skip
people = list(self.Person.objects[1:].scalar('name'))
self.assertEqual(len(people), 2)
self.assertEqual(people[0], 'User B')
self.assertEqual(people[1], 'User C')
# Test slice limit and skip
people = list(self.Person.objects[1:2].scalar('name'))
self.assertEqual(len(people), 1)
self.assertEqual(people[0], 'User B')
people = list(self.Person.objects[1:1].scalar('name'))
self.assertEqual(len(people), 0)
# Test slice out of range
people = list(self.Person.objects.scalar('name')[80000:80001])
self.assertEqual(len(people), 0)
# Test larger slice __repr__
self.Person.objects.delete()
for i in xrange(55):
self.Person(name='A%s' % i, age=i).save()
self.assertEqual(len(self.Person.objects.scalar('name')), 55)
self.assertEqual("A0", "%s" % self.Person.objects.order_by('name').scalar('name').first())
self.assertEqual("A0", "%s" % self.Person.objects.scalar('name').order_by('name')[0])
self.assertEqual("[u'A1', u'A2']", "%s" % self.Person.objects.order_by('age').scalar('name')[1:3])
self.assertEqual("[u'A51', u'A52']", "%s" % self.Person.objects.order_by('age').scalar('name')[51:53])
# with_id and in_bulk
person = self.Person.objects.order_by('name').first()
self.assertEqual("A0", "%s" % self.Person.objects.scalar('name').with_id(person.id))
pks = self.Person.objects.order_by('age').scalar('pk')[1:3]
self.assertEqual("[u'A1', u'A2']", "%s" % sorted(self.Person.objects.scalar('name').in_bulk(list(pks)).values()))
class QTest(unittest.TestCase):
@@ -2790,6 +3399,30 @@ class QueryFieldListTest(unittest.TestCase):
q += QueryFieldList(fields=['a'], value={"$slice": 5})
self.assertEqual(q.as_dict(), {'a': {"$slice": 5}})
def test_elem_match(self):
class Foo(EmbeddedDocument):
shape = StringField()
color = StringField()
trick = BooleanField()
meta = {'allow_inheritance': False}
class Bar(Document):
foo = ListField(EmbeddedDocumentField(Foo))
meta = {'allow_inheritance': False}
Bar.drop_collection()
b1 = Bar(foo=[Foo(shape= "square", color ="purple", thick = False),
Foo(shape= "circle", color ="red", thick = True)])
b1.save()
b2 = Bar(foo=[Foo(shape= "square", color ="red", thick = True),
Foo(shape= "circle", color ="purple", thick = False)])
b2.save()
ak = list(Bar.objects(foo__match={'shape': "square", "color": "purple"}))
self.assertEqual([b1], ak)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,32 @@
import unittest
import pymongo
from pymongo import ReadPreference, ReplicaSetConnection
import mongoengine
from mongoengine import *
from mongoengine.connection import get_db, get_connection, ConnectionError
class ConnectionTest(unittest.TestCase):
def tearDown(self):
mongoengine.connection._connection_settings = {}
mongoengine.connection._connections = {}
mongoengine.connection._dbs = {}
def test_replicaset_uri_passes_read_preference(self):
"""Requires a replica set called "rs" on port 27017
"""
try:
conn = connect(db='mongoenginetest', host="mongodb://localhost/mongoenginetest?replicaSet=rs", read_preference=ReadPreference.SECONDARY_ONLY)
except ConnectionError, e:
return
if not isinstance(conn, ReplicaSetConnection):
return
self.assertEquals(conn.read_preference, ReadPreference.SECONDARY_ONLY)
if __name__ == '__main__':
unittest.main()

View File

@@ -56,6 +56,18 @@ class SignalTests(unittest.TestCase):
@classmethod
def post_delete(cls, sender, document, **kwargs):
signal_output.append('post_delete signal, %s' % document)
@classmethod
def pre_bulk_insert(cls, sender, documents, **kwargs):
signal_output.append('pre_bulk_insert signal, %s' % documents)
@classmethod
def post_bulk_insert(cls, sender, documents, **kwargs):
signal_output.append('post_bulk_insert signal, %s' % documents)
if kwargs.get('loaded', False):
signal_output.append('Is loaded')
else:
signal_output.append('Not loaded')
self.Author = Author
@@ -104,7 +116,9 @@ class SignalTests(unittest.TestCase):
len(signals.pre_save.receivers),
len(signals.post_save.receivers),
len(signals.pre_delete.receivers),
len(signals.post_delete.receivers)
len(signals.post_delete.receivers),
len(signals.pre_bulk_insert.receivers),
len(signals.post_bulk_insert.receivers),
)
signals.pre_init.connect(Author.pre_init, sender=Author)
@@ -113,6 +127,8 @@ class SignalTests(unittest.TestCase):
signals.post_save.connect(Author.post_save, sender=Author)
signals.pre_delete.connect(Author.pre_delete, sender=Author)
signals.post_delete.connect(Author.post_delete, sender=Author)
signals.pre_bulk_insert.connect(Author.pre_bulk_insert, sender=Author)
signals.post_bulk_insert.connect(Author.post_bulk_insert, sender=Author)
signals.pre_init.connect(Another.pre_init, sender=Another)
signals.post_init.connect(Another.post_init, sender=Another)
@@ -128,6 +144,8 @@ class SignalTests(unittest.TestCase):
signals.pre_delete.disconnect(self.Author.pre_delete)
signals.post_save.disconnect(self.Author.post_save)
signals.pre_save.disconnect(self.Author.pre_save)
signals.pre_bulk_insert.disconnect(self.Author.pre_bulk_insert)
signals.post_bulk_insert.disconnect(self.Author.post_bulk_insert)
signals.pre_init.disconnect(self.Another.pre_init)
signals.post_init.disconnect(self.Another.post_init)
@@ -143,7 +161,9 @@ class SignalTests(unittest.TestCase):
len(signals.pre_save.receivers),
len(signals.post_save.receivers),
len(signals.pre_delete.receivers),
len(signals.post_delete.receivers)
len(signals.post_delete.receivers),
len(signals.pre_bulk_insert.receivers),
len(signals.post_bulk_insert.receivers),
)
self.assertEqual(self.pre_signals, post_signals)
@@ -154,6 +174,14 @@ class SignalTests(unittest.TestCase):
def create_author():
a1 = self.Author(name='Bill Shakespeare')
def bulk_create_author_with_load():
a1 = self.Author(name='Bill Shakespeare')
self.Author.objects.insert([a1], load_bulk=True)
def bulk_create_author_without_load():
a1 = self.Author(name='Bill Shakespeare')
self.Author.objects.insert([a1], load_bulk=False)
self.assertEqual(self.get_signal_output(create_author), [
"pre_init signal, Author",
"{'name': 'Bill Shakespeare'}",
@@ -178,4 +206,25 @@ class SignalTests(unittest.TestCase):
self.assertEqual(self.get_signal_output(a1.delete), [
'pre_delete signal, William Shakespeare',
'post_delete signal, William Shakespeare',
])
])
signal_output = self.get_signal_output(bulk_create_author_with_load)
# The output of this signal is not entirely deterministic. The reloaded
# object will have an object ID. Hence, we only check part of the output
self.assertEquals(signal_output[3],
"pre_bulk_insert signal, [<Author: Bill Shakespeare>]")
self.assertEquals(signal_output[-2:],
["post_bulk_insert signal, [<Author: Bill Shakespeare>]",
"Is loaded",])
self.assertEqual(self.get_signal_output(bulk_create_author_without_load), [
"pre_init signal, Author",
"{'name': 'Bill Shakespeare'}",
"post_init signal, Bill Shakespeare",
"pre_bulk_insert signal, [<Author: Bill Shakespeare>]",
"post_bulk_insert signal, [<Author: Bill Shakespeare>]",
"Not loaded",
])
self.Author.objects.delete()