Compare commits

..

386 Commits

Author SHA1 Message Date
Omer Katz
cbd2a44350 Changed an invalid classifier to a valid one. 2015-03-28 11:34:22 +03:00
Ross Lawley
c888e461ba Updated travis.yml to build release tags 2015-03-24 17:09:26 +00:00
David Bordeynik
d135522087 Adding #714 to changelog and AUTHORS 2015-03-23 15:46:12 +02:00
J. Fernando Sánchez
ce2b148dd2 Fixes #714 2015-03-23 12:21:21 +02:00
J. Fernando Sánchez
2d075c4dd6 Added test for new_file after saved as none. #714 2015-03-23 12:21:21 +02:00
Omer Katz
bcd1841f71 Enabled to release mongoengine to PyPi when tagging. 2015-03-23 11:52:24 +02:00
Omer Katz
029cf4ad1f Merge pull request #908 from rozza/travis
Updated travis build
2015-03-18 18:52:38 +02:00
Omer Katz
ed7fc86d69 Merge pull request #901 from elasticsales/fix-test-with-profiling
Fix the unit tests for mongodb w/ profiling enabled
2015-03-18 16:38:31 +02:00
Ross Lawley
82a9e43b6f Updated travis build
Fixed pymongo versions to 2.7.2 and 2.8
The dev build for pymongo is the master branch which points to the new 3.0 driver.
Support for 3.0 will need to be added separately in the future as its a major version change and
will contain some backwards breaking changes
2015-03-18 13:55:16 +00:00
Stefan Wojcik
9ae2c731ed dont drop any system collections 2015-03-03 14:30:09 -08:00
Omer Katz
7d1ba466b4 Merge pull request #893 from MongoEngine/topic/pop-default-argument
Use dict.pop() default argument instead of checking if the key exists ourselves
2015-03-01 17:34:10 +02:00
Omer Katz
4f1d8678ea Merge pull request #896 from MongoEngine/topic/remove-mutable-arguments
Use None instead of mutable arguments
2015-03-01 15:04:27 +02:00
Omer Katz
4bd72ebc63 Use None instead of mutable arguments. 2015-02-27 11:32:06 +02:00
Omer Katz
e5986e0ae2 Use dict.pop() default argument instead of checking if the key exists ourselves. 2015-02-27 11:18:09 +02:00
David Bordeynik
fae39e4bc9 Merge pull request #886 from rutsky/patch-1
fix typo: "a the structure" -> "the structure"
2015-02-26 11:38:41 +02:00
David Bordeynik
dbe8357dd5 Merge pull request #883 from jerrysxu/patch-1
Update fields.py
2015-02-26 11:38:33 +02:00
David Bordeynik
3234f0bdd7 Merge pull request #887 from rutsky/patch-2
fix reference format: "attr:`auto_create_index`"
2015-02-20 22:18:39 +02:00
David Bordeynik
47a4d58009 Merge pull request #826 from seglberg/mmelliso/fix#503
EmbeddedDocumentListField (Resolves #503)
2015-02-20 21:53:18 +02:00
Vladimir Rutsky
4ae60da58d fix reference format: "attr:auto_create_index" 2015-02-20 19:59:36 +03:00
Vladimir Rutsky
47f995bda3 fix typo: "a the structure" -> "the structure" 2015-02-20 19:41:20 +03:00
Matthew Ellison
42721628eb Added EmbeddedDocumentListField Implementation
- Added new field type: EmbeddedDocumentListField.
- Provides additional query ability for lists of embedded documents.
- Closes MongoEngine/mongoengine#503.
2015-02-20 11:18:40 -05:00
David Bordeynik
f42ab957d4 Merge pull request #885 from DavidBord/fix-864
Fix #864: ComplexDateTimeField should fall back to None when null=True
2015-02-19 11:46:03 +02:00
Jimmy Shen
ce9d0d7e82 Fix #864: ComplexDateTimeField should fall back to None when null=True 2015-02-19 09:47:38 +02:00
David Bordeynik
baf79dda21 Merge pull request #881 from DavidBord/fix-863
Fix #863: Request Support for $min, $max Field update operators
2015-02-18 11:13:25 +02:00
Jerry Xu
b71a9bc097 Update fields.py
Type name should be "MultiPolygon".
2015-02-17 13:22:02 -08:00
David Bordeynik
129632cd6b Fix #863: Request Support for $min, $max Field update operators 2015-02-17 21:48:25 +02:00
David Bordeynik
aca8899c4d Merge pull request #879 from DavidBord/fix-866
Fix #866:  does not follow
2015-02-16 15:49:06 +02:00
David Bordeynik
5c3d91e65e Fix #866: does not follow 2015-02-16 12:25:37 +02:00
David Bordeynik
0205d827f1 Merge pull request #876 from DavidBord/fix-766
Fix #766: Add support for  operator
2015-02-15 22:05:24 +02:00
David Bordeynik
225c31d583 Fix #766: Add support for operator 2015-02-15 15:05:07 +02:00
David Bordeynik
b18d87ddba Merge pull request #878 from DavidBord/fix-877
Fix #877: Fix tests for pymongo 2.8+
2015-02-15 15:00:35 +02:00
David Bordeynik
25298c72bb Fix #877: Fix tests for pymongo 2.8+ 2015-02-15 10:02:22 +02:00
David Bordeynik
3df3d27533 Merge pull request #873 from DavidBord/fix-872
Fix #872: No module named 'django.utils.importlib' (Django dev)
2015-02-15 09:31:38 +02:00
David Bordeynik
cbb0b57018 Fix #872: No module named 'django.utils.importlib' (Django dev) 2015-02-15 00:10:00 +02:00
David Bordeynik
65f205bca8 Merge pull request #848 from seglberg/choice-subclasses
Field Choices Now Accept Subclasses of Documents
2015-02-06 09:32:47 +02:00
Yohan Graterol
1cc7f80109 Merge pull request #845 from aeroeng/andQ
mongo $and list should not contain list elements in order to avoid this ...
2015-01-22 20:13:53 -05:00
Matthew Ellison
213a0a18a5 Updated Unit Tests for Field Choices of Documents
- Added Unit Test with Invalid EmbeddedDocument Choice.
- Updated Broken Link in Author's File
2015-01-12 10:11:42 -05:00
Matthew Ellison
1a24d599b3 Field Choices Now Accept Subclasses of Documents
- Fields containing 'choices' of which a choice is an
  EmbeddedDocument or Document will now accept subclasses of that
  choice.
2015-01-11 20:54:59 -05:00
aeroeng
d80be60e2b mongo $and list should not contain list elements in order to avoid this error:
$and/$or elements must be objects
2015-01-06 14:49:29 -05:00
Yohan Graterol
0ffe79d76c Merge pull request #823 from mmelliso/mmelliso/fix#812
Ensure Indexes before Each Save (Resolves #812)
2014-12-05 11:01:02 -05:00
Matthew Ellison
db36d0a375 Ensure Indexes before Each Save
- Rely on caching within the PyMongo driver to provide lightweight calls
  while indices are cached.
- Closes MongoEngine/mongoengine#812.
2014-12-04 08:45:15 -05:00
Omer Katz
ff659a0be3 Merge pull request #815 from MRigal/master
fixed bug for queryset.distinct to work also on embedded documents, not ...
2014-12-04 14:21:04 +02:00
Wilson Júnior
8485b12102 Merge pull request #821 from 3lnc/Document.switch_docstring_fix
Fixes #811. Fixes reflinks
2014-12-04 09:28:11 -02:00
Wilson Júnior
d889cc3c5a Merge pull request #825 from idlead/fix/reverse_delete_rules_on_abstract_documents
Fix crash when applying deletion rules
2014-12-04 09:23:59 -02:00
Antoine Français
7bb65fca4e Fix crash when applying deletion rules
When deleting a document references by other, if that refence is
defined on an abstract document, the operation fails, because it tries
to apply deletion on the abstract class which doesn't have a QuerySet.

Fix is simply to ignore document classes which are defined abstract when
applying rules on all classes referencing the document.
2014-12-04 11:34:23 +01:00
mrigal
8aaa5951ca fixed order of list for the test to pass 2014-12-03 13:13:07 +01:00
Matthieu Rigal
d58f3b7520 Merge pull request #1 from thedrow/patch-1
Added a test that verifies distinct operations on nested embedded docume...
2014-12-03 13:10:27 +01:00
Omer Katz
e5a636a159 Added a test that verifies distinct operations on nested embedded documents. 2014-12-03 11:09:05 +02:00
Slam
51f314e907 Doc fixes, thanks @3Inc
Author:    Slam <3lnc.slam@gmail.com>
Date:      Fri Nov 28 13:10:38 2014 +0200
2014-12-02 00:37:06 -02:00
mrigal
531fa30b69 added test for capacity to get distinct on subdocument of a list 2014-12-01 18:20:29 +01:00
Wilson Júnior
2b3bb81fae Refactoring: Simple is better than complex
Signed-off-by: Wilson Júnior <wilsonpjunior@gmail.com>
2014-11-29 23:48:58 -02:00
Rik
80f80cd31f fixed more tests that were using undefined model fields 2014-11-29 23:20:31 -02:00
Rik
79705fbf11 moved initialization of _created before FieldDoesNotExist check
Because otherwise we'll get a FieldDoesNotExist error on the field
_created.
2014-11-29 23:20:30 -02:00
Rik
191a4e569e added ints in string.format() for 2.6 compability 2014-11-29 23:20:30 -02:00
Rik
1cac35be03 using python 2.6 compatible way of assertRaises 2014-11-29 23:20:30 -02:00
Rik
6d48100f44 add test if FieldDoesNotExist is raised
When trying to set an undefined field.
2014-11-29 23:20:30 -02:00
Rik
4627af3e90 add FieldDoesNotExist exception to __all__
So it will be available when you do:
    from mongoengine import *
2014-11-29 23:20:30 -02:00
Rik
913952ffe1 remove unittest test_no_overwritting_no_data_loss
Now that fields need to be defined explicitly, it's not possible to have
another property with the same name on a model.
https://github.com/MongoEngine/mongoengine/pull/457#issuecomment-47513105
2014-11-29 23:20:30 -02:00
Rik
67bf6afc89 fixed tests that were using undefined model fields 2014-11-29 23:20:30 -02:00
Rik
06064decd2 check for dynamic document, exclude id pk and _cls 2014-11-29 23:20:30 -02:00
Rik
4cca9f17df Check if undefined fields are supplied on document
If an undefined field is supplied to a document instance, a
`FieldDoesNotExist` Exception will be raised.
2014-11-29 23:20:30 -02:00
Wilson Júnior
74a89223c0 replaced text_score attribute to get_text_score method
Signed-off-by: Wilson Júnior <wilsonpjunior@gmail.com>
2014-11-29 23:09:26 -02:00
Slam
2954017836 Fixes #811. Fixes reflinks 2014-11-30 00:23:40 +02:00
mrigal
a03262fc01 implemented ability to return instances and not simple dicts for distinct on subdocuments 2014-11-28 16:23:23 +01:00
mrigal
d65ce6fc2c fixed bug for queryset.distinct to work also on embedded documents, not just on lists of embedded documents 2014-11-28 13:54:33 +01:00
Yohan Graterol
d27e1eee25 Merge pull request #806 from mmelliso/mmelliso/indexing
Generate Unique Indices for Lists of EmbeddedDocs
2014-11-25 02:37:53 -05:00
Yohan Graterol
b1f00bb708 Merge pull request #810 from 3lnc/Doc_fixes
Minor typos fixes in docs
2014-11-25 02:36:59 -05:00
Slam
e0f1e79e6a Minor typos fixes in docs 2014-11-24 16:57:43 +02:00
Matthew Ellison
d70b7d41e8 Update to Changelog to include Fix for #358 2014-11-21 07:29:50 -05:00
Matthew Ellison
43af9f3fad Update Tests for EmbeddedDocument Unique Indicies 2014-11-20 11:20:04 -05:00
Matthew Ellison
bc53dd6830 Generate Unique Indices for Lists of EmbeddedDocs
- Unique indices are now created in the database for EmbeddedDocument
  fields when the EmbeddedDocument is in a ListField
- Closes Issue #358
2014-11-19 22:37:27 -05:00
Yohan Graterol
263616ef01 Merge pull request #804 from CestDiego/patch-1
Big typo fix for allow_inheritance page
2014-11-19 21:20:02 -05:00
Diego Berrocal
285da0542e Update AUTHORS with @cestdiego 2014-11-19 09:39:51 -05:00
Diego Berrocal
17f7e2f892 Big typo fix for allow_inheritance page 2014-11-19 02:49:08 -05:00
Yohan Graterol
a29d8f1d68 Merge pull request #803 from DavidBord/fix-515
fix-#515: sparse fields
2014-11-16 22:04:24 -05:00
David Bordeynik
8965172603 fix-#515: sparse fields 2014-11-14 21:45:46 +02:00
David Bordeynik
03c2967337 Update changelog & authors - #801 2014-11-13 20:47:31 +02:00
David Bordeynik
5b154a0da4 Merge pull request #801 from mikhailmoshnogorsky/patch-1
write_concern not in params of Collection#remove
2014-11-13 16:19:56 +02:00
mikhailmoshnogorsky
b2c8c326d7 write_concern not in params of Collection#remove 2014-11-12 17:00:07 -05:00
Yohan Graterol
96aedaa91f Install Django dev from repo with pip 2014-11-12 12:06:20 -05:00
Omer Katz
a22ad1ec32 Exclude Django 1,7 and Python 2.6 since Django 1.7 doesn't support 2.6. 2014-11-11 09:09:21 +02:00
Omer Katz
a4244defb5 Fixed build matrix. 2014-11-10 09:41:29 +02:00
Omer Katz
57328e55f3 Bumped django versions and added 1.7.1. 2014-11-10 09:37:35 +02:00
Yohan Graterol
87c32aeb40 Merge branch 'aericson-better_basedocument_eq' 2014-11-09 21:32:58 -05:00
Yohan Graterol
2e01e0c30e Added merge to changelog.rst 2014-11-09 21:32:50 -05:00
Yohan Graterol
a12b2de74a Fix merge MongoEngine/mongoengine#799 2014-11-09 21:31:56 -05:00
Yohan Graterol
6b01d8f99b Merge branch 'DavidBord-fix-734' 2014-11-09 21:28:12 -05:00
Yohan Graterol
eac4f6062e Fix merge in docs/changelog.rst 2014-11-09 21:28:03 -05:00
Yohan Graterol
5583cf0a5f PEP8 compliance tests/document/instance.py 2014-11-09 21:27:23 -05:00
Yohan Graterol
57d772fa23 Fix merge in tests/document/instance.py 2014-11-09 21:19:05 -05:00
Yohan Graterol
1bdc3988a9 Merge pull request #798 from DavidBord/fix-771
fix-#771: OperationError: Shard Keys are immutable. Tried to update id e...
2014-11-09 20:51:06 -05:00
André Ericson
2af55baa9a Better BaseDocument equality check when not saved
When 2 instances of a Document had id = None they would be considered
equal unless an __eq__ were implemented.

We now return False for such case. It now behaves more similar to
Django's ORM.
2014-11-09 16:19:15 -03:00
David Bordeynik
0452eec11d fix-#771: OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved 2014-11-09 19:23:49 +02:00
Wilson Júnior
c4f7db6c04 Merge pull request #796 from aericson/fix_dynamic_document_reload
Fix KeyError on reload() from a DynamicDocument
2014-11-08 23:52:37 -02:00
André Ericson
3569529a84 Fix KeyError on reload() from a DynamicDocument
If the document is in memory and a field is deleted from the db,
calling reload() would raise a KeyError.
2014-11-08 19:11:51 -03:00
Yohan Graterol
70942ac0f6 Update changelog.rst 2014-11-07 11:03:49 -05:00
Yohan Graterol
dc02e39918 Merge branch 'a4tunado-753' 2014-11-07 11:03:02 -05:00
Yohan Graterol
73d6bc35ec Fix merge with AUTHORS 2014-11-07 11:02:48 -05:00
Yohan Graterol
b1d558d700 Merge branch 'DavidBord-fix-787' 2014-11-07 10:55:56 -05:00
Yohan Graterol
897480265f fix PR #787 2014-11-07 10:55:46 -05:00
Yohan Graterol
73724f5a33 Merge pull request #793 from DavidBord/fix-759
fix-#759: with_limit_and_skip for count should default like in pymongo
2014-11-07 10:53:04 -05:00
DavidBord
bdbd495a9e fix-#734: set attribute to None does not work (at least for fields with default values). Solves #735 as well 2014-11-07 15:11:21 +02:00
DavidBord
1fcf009804 fix-#787: Fix storing value of precision attribute in DecimalField 2014-11-07 15:03:11 +02:00
DavidBord
914c5752a5 fix-#759: with_limit_and_skip for count should default like in pymongo 2014-11-07 09:21:17 +02:00
DavidBord
201b12a886 Merge pull request #774 from DavidBord/fix-744
fix-#744: Querying by a field defined in a subclass raises InvalidQueryE...
2014-11-06 08:10:31 +02:00
DavidBord
c5f23ad93d fix-#744: Querying by a field defined in a subclass raises InvalidQueryError 2014-11-06 00:15:23 +02:00
Yohan Graterol
28d62009a7 Update changelog.rst 2014-11-05 14:56:13 -05:00
Yohan Graterol
1a5a436f82 Merge pull request #775 from claymation/in_bulk_honors_no_dereference
Make `in_bulk()` respect `no_dereference()`
2014-11-05 14:55:43 -05:00
Yohan Graterol
1275ac0569 Merge pull request #773 from KonishchevDmitry/pr-document-modify
Add Document.modify() method
2014-11-02 17:13:33 -05:00
Dmitry Konishchev
5112fb777e Mention Document.modify() in the documentation 2014-11-02 17:56:25 +03:00
Dmitry Konishchev
f571a944c9 Add #773 to changelog 2014-11-02 17:52:44 +03:00
Dmitry Konishchev
bc9aff8c60 Merge remote-tracking branch 'upstream/master' into pr-document-modify 2014-11-02 17:24:51 +03:00
Yohan Graterol
c4c7ab7888 Merge pull request #770 from yjaaidi/patch-1
Version bump 0.8.7 => 0.9.0
2014-11-01 14:37:51 -05:00
Yohan Graterol
d9819a990c Merge pull request #772 from shuuji3/patch-1
Marked up the last line as preformatted text.
2014-11-01 14:37:44 -05:00
Yohan Graterol
aea400e26a Merge pull request #791 from czarneckid/fix-reverse-delete-rule-documentation
Fix the documentation for reverse_delete_rule.
2014-11-01 14:32:48 -05:00
David Czarnecki
eb4e7735c1 Adding myself to AUTHORS and CHANGELOG 2014-11-01 13:15:02 -04:00
David Czarnecki
4b498ae8cd Fix the documentation for reverse_delete_rule. 2014-10-31 11:40:20 -04:00
DavidBord
158e2a4ca9 Merge pull request #779 from DavidBord/fix-778
fix-#778: Add Support For MongoDB 2.6.X's maxTimeMS
2014-10-29 17:54:40 +02:00
DavidBord
b011d48d82 fix-#778: Add Support For MongoDB 2.6.X's maxTimeMS 2014-10-29 15:40:29 +02:00
DavidBord
8ac3e725f8 Merge pull request #790 from DavidBord/fix-789
fix-#789: abstract shouldn't be inherited in EmbeddedDocument
2014-10-29 13:39:56 +02:00
DavidBord
9a4aef0358 fix-#789: abstract shouldn't be inherited in EmbeddedDocument 2014-10-29 13:36:42 +02:00
Clay McClure
7d3146234a Make in_bulk() respect no_dereference() 2014-10-01 15:59:13 -04:00
Dmitry Konishchev
5d2ca6493d Drop unnecessary id=ObjectId() in document creation 2014-10-01 12:38:41 +04:00
Dmitry Konishchev
4752f9aa37 Add Document.modify() method 2014-09-30 15:30:01 +04:00
Shuuji TAKAHASHI
025d3a03d6 Marked up the last line as preformatted text. 2014-09-30 02:31:48 +09:00
yjaaidi
aec06183e7 Version bump 0.8.7 => 0.9.0 2014-09-28 10:09:36 +02:00
Yohan Graterol
aa28abd517 Merge pull request #750 from foxx/patch-1
MongoTestCase relies on "nose"
2014-09-04 10:07:50 -05:00
Vjacheslav Murashkin
7430b31697 handle None from model __str__; Fixes #753 2014-09-04 16:54:23 +04:00
Yohan Graterol
759f72169a Merge pull request #751 from noirbizarre/patch-1
noirbizarre to AUTHORS
2014-09-02 21:48:16 -05:00
Axel Haustant
1f7135be61 Added myself to AUTHORS 2014-09-03 03:15:26 +02:00
Yohan Graterol
6942f9c1cf Merge pull request #748 from bocribbz/fix-multiple-connections
Fix multiple connections aliases being rewritten
2014-09-01 20:30:18 -05:00
Bob Cribbs
d9da75d1c0 Fix multiple connections aliases being rewritten 2014-09-01 23:26:01 +03:00
Cal Leeming
7ab7372be4 MongoTestCase relies on "nose" 2014-09-01 20:40:01 +01:00
Yohan Graterol
3503c98857 Merge pull request #749 from noirbizarre/multigeo
Multi geometry fields support
2014-09-01 11:21:08 -05:00
Axel Haustant
708c3f1e2a Added new geospatial fields to th documentation 2014-08-28 19:42:09 +02:00
Axel Haustant
6f645e8619 Added MultiPoint, MultiLine and MultiPolygon fields 2014-08-28 19:36:29 +02:00
Yohan Graterol
bce7ca7ac4 Fix merge of PR #747 2014-08-27 11:14:43 -05:00
Yohan Graterol
350465c25d Change with PR #719 and close mongoengine/Issue #397 2014-08-26 10:50:07 -05:00
Yohan Graterol
5b9c70ae22 Merge pull request #719 from DavidBord/fix-397
fix-#397: Allow specifying the '_cls' as a field for indexes
2014-08-26 10:48:10 -05:00
DavidBord
9b30afeca9 fix-#397: Allow specifying the '_cls' as a field for indexes 2014-08-24 10:51:49 +03:00
DavidBord
c1b202c119 fix-#746: Stop ensure_indexes running on a secondaries unless connection is through mongos 2014-08-24 10:48:54 +03:00
Yohan Graterol
41cfe5d2ca Update changelog.rst 2014-08-22 08:24:41 -05:00
Yohan Graterol
05339e184f Merge pull request #737 from wcdolphin/master
Make 'db' argument to connection optional
2014-08-22 08:24:03 -05:00
wcdolphin
447127d956 Makes 'db' argument to connection optional. 2014-08-21 16:08:31 -07:00
Yohan Graterol
394334fbea Merge branch 'jshirley-master' 2014-08-20 11:06:09 -05:00
Yohan Graterol
9f8cd33d43 Fix conflict for merge PR #726 2014-08-20 11:05:53 -05:00
Yohan Graterol
f066e28c35 Update changelog.rst 2014-08-19 23:14:00 -05:00
Yohan Graterol
b349a449bb Merge pull request #742 from bocribbz/dictfield-atomic-update
Allow atomic update for the entire `DictField`
2014-08-19 23:13:37 -05:00
Jay Shirley
1c5898d396 Adding changelog entry. 2014-08-19 15:54:29 -07:00
Jay Shirley
6802967863 Merge remote-tracking branch 'upstream/master' 2014-08-19 15:52:58 -07:00
Bob Cribbs
0462f18680 Allow atomic update for the entire DictField 2014-08-19 23:38:36 +03:00
Yohan Graterol
af6699098f Update .travis.yml for allow failures in pypy3 2014-08-19 10:43:22 -05:00
Yohan Graterol
6b7e7dc124 Update for add the change that closes issue #733 2014-08-17 22:43:36 -05:00
Yohan Graterol
6bae4c6a66 Merge pull request #738 from DavidBord/fix-733
fix-#733: index_cls is ignored when deciding to set _cls as index prefix
2014-08-17 22:41:41 -05:00
DavidBord
46da918dbe fix-#733: index_cls is ignored when deciding to set _cls as index prefix 2014-08-17 11:19:18 +03:00
Ross Lawley
bb7e5f17b5 Update .travis.yml
Added coveralls coverage...
2014-08-12 14:52:39 +01:00
Yohan Graterol
b9d03114c2 Merge pull request #728 from MongoEngine/topic/landscape-io
Added landscape.io badge
2014-08-11 08:31:11 -05:00
Omer Katz
436b1ce176 Added PyMongo 2.7.2 to the build matrix. 2014-08-10 18:44:30 +03:00
Omer Katz
50fb5d83f1 Added landscape.io badge. 2014-08-10 18:26:18 +03:00
Wilson Júnior
fda672f806 Merge pull request #727 from DavidBord/fix-725
fix-#725: queryset delete() should return the number of deleted objects
2014-08-10 11:43:59 -03:00
Omer Katz
2bf783b04d Added PyPy3 to the build matrix. 2014-08-10 17:01:18 +03:00
DavidBord
2f72b23a0d fix-#725: queryset delete() should return the number of deleted objects 2014-08-10 14:58:39 +03:00
Jay Shirley
85336f9777 Relax the RegEx restrictions to allow the new ICAAN TLDs. 2014-08-08 09:11:05 -07:00
Yohan Graterol
174d964553 Update changelog.rst 2014-08-06 01:54:42 -05:00
Yohan Graterol
cf8677248e Merge pull request #723 from DavidBord/fix-620
Fix 620
2014-08-06 01:53:28 -05:00
DavidBord
1e6a3163af fix-#620: saving document doesn't create new fields in existing collection 2014-08-05 17:29:14 +03:00
DavidBord
e008919978 fix-#399: Not overriding default values when loading a subset of fields 2014-08-05 14:34:54 +03:00
Wilson Júnior
4814066c67 Merge pull request #709 from wpjunior/cached-reference-field
CachedReferenceField implementation
2014-08-03 21:38:06 -03:00
Wilson Junior
f17f8b48c2 small fixes for python2.6 2014-08-03 18:59:50 -04:00
Yohan Graterol
ab0aec0ac5 Merge pull request #720 from pashadia/master
Fixed typo.
2014-08-03 14:04:15 -05:00
pashadia
b49a641ba5 Fixed typo. 2014-08-03 20:44:46 +03:00
Yohan Graterol
2f50051426 Merge pull request #718 from mbalasso/patch-1
Update README.rst
2014-08-01 10:46:50 -05:00
Matteo Balasso
43cc32db40 Update README.rst 2014-08-01 16:35:53 +02:00
Wilson Júnior
b4d6f6b947 added documentation about CachedReferenceField 2014-07-30 09:32:33 -03:00
Omer Katz
71ff533623 Updated Django development version in the build matrix. 2014-07-30 02:02:21 +03:00
Wilson Júnior
e33a5bbef5 fixes for python2.6 2014-07-26 07:24:04 -03:00
Wilson Júnior
6c0112c2be refs #709, added support to disable auto_sync 2014-07-25 18:12:26 -03:00
Wilson Júnior
15bbf26b93 refs #709, fix typos 2014-07-25 08:48:24 -03:00
Wilson Júnior
87c97efce0 refs #709, added CachedReferenceField.sync_all to sync all documents on demand 2014-07-25 08:44:59 -03:00
Wilson Júnior
6c4aee1479 added CachedReferenceField restriction to use in EmbeddedDocument 2014-07-17 13:42:34 -03:00
Wilson Júnior
73549a9044 fixes for rebase branch 2014-07-17 09:41:06 -03:00
Wilson Júnior
30fdd3e184 Added initial CachedReferenceField 2014-07-16 10:32:43 -03:00
Wilson Júnior
c97eb5d63f Added retry in apt-get operations for travis 2014-07-14 16:05:49 -03:00
Wilson Júnior
5729c7d5e7 Merge branch 'master' of https://github.com/MongoEngine/mongoengine 2014-07-14 16:00:26 -03:00
Wilson Júnior
d77b13efcb Merge pull request #703 from wpjunior/aggregate-framework
Simple aggregation framework wrapper
2014-07-14 14:29:59 -03:00
Wilson Júnior
c43faca7b9 refs #703, added changelog 2014-07-13 09:55:46 -03:00
Wilson Júnior
892ddd5724 added a wrapper to aggregate in Queryset.aggregate 2014-07-12 23:18:08 -03:00
Wilson Júnior
a9de779f33 Merge pull request #701 from yograterol/master
Remove allow_failures from .travis.yml file
2014-07-08 14:21:39 -03:00
Yohan Graterol
1c2f016ba0 Remove allow_failures from .travis.yml file 2014-07-08 10:25:12 -05:00
Wilson Júnior
7b4d9140af merge #700 2014-07-08 09:19:15 -03:00
Wilson Júnior
c1fc87ff4e added entry in changelog 2014-07-08 09:16:01 -03:00
Wilson Júnior
cd5ea5d4e0 testing a travis with mongodb 2.6 2014-07-08 08:49:03 -03:00
Wilson Júnior
30c01089f5 added ordering support for text queries 2014-07-08 08:38:41 -03:00
Wilson Júnior
89825a2b21 added skip to mongodb older 2014-07-07 23:45:44 -03:00
Wilson Júnior
a743b75bb4 fixed a order in command 2014-07-07 21:02:13 -03:00
Wilson Júnior
f7ebf8dedd Added support for text search and text_score. 2014-07-07 20:24:37 -03:00
Yohan Graterol
f6220cab3b Merge pull request #697 from nleite/master
to_json not resolving db_fields #654
2014-07-07 17:39:34 -05:00
Norberto
0c5e1c4138 adding myself to authors 2014-07-07 22:07:01 +02:00
Norberto
03fe431f1a merge with origin done 2014-07-07 21:59:42 +02:00
Norberto
a8e4554fec Update change log on #697 merge 2014-07-07 19:27:39 +02:00
Norberto
e81b09b9aa adding capability to extract json (to_json) using the field names and instead of the defined db_names, if those are set 2014-07-06 20:49:19 +02:00
Yohan Graterol
c6e846e0ae Merge pull request #696 from lexqt/fix_django_17_compat
Fix tests for django 1.7
2014-07-06 12:43:12 -05:00
Aleksey Porfirov
03dcfb5c4b Update changelog 2014-07-06 12:27:34 +04:00
Aleksey Porfirov
3e54da03e2 Fix MongoTestCase and add test for it 2014-07-05 21:35:31 +04:00
Aleksey Porfirov
c4b3196917 Fix MongoTestCase and add test for it 2014-07-05 21:13:25 +04:00
Aleksey Porfirov
0d81e7933e Prevent accessing not yet configured settings in django.MongoTestCase 2014-07-05 00:06:10 +04:00
Aleksey Porfirov
b2a2735034 Update AUTHORS 2014-07-04 22:32:07 +04:00
Aleksey Porfirov
f865c5de90 Fix tests for Django 1.7 2014-07-04 22:30:29 +04:00
Yohan Graterol
4159369e8b Merge pull request #690 from claymation/select-related
Follow ReferenceFields in EmbeddedDocuments with select_related
2014-07-03 15:28:09 -05:00
Clay McClure
170693cf0b Follow ReferenceFields in EmbeddedDocuments with select_related
For the following structure:

    class Playlist(Document):
        items = ListField(EmbeddedDocumentField("PlaylistItem"))

    class PlaylistItem(EmbeddedDocument):
        song = ReferenceField("Song")

    class Song(Document):
        title = StringField()

this patch prevents the N+1 queries otherwise required to fetch all
the `Song` instances referenced by all the `PlaylistItem`s.
2014-07-03 13:14:45 -04:00
Omer Katz
4e7b5d4af8 Added @rochacbrun to the AUTHORS file. #692 2014-07-03 17:35:55 +03:00
Omer Katz
67bf789fcf Merge pull request #692 from rochacbruno/master
Updates with no operator should default to $set Fix #667
2014-07-03 09:02:12 +03:00
Omer Katz
f5cf616c2f Merge pull request #635 from SergeChmelev/master
Fix for post_init signal to receive correct state of _created flag.
2014-07-03 02:38:52 +03:00
Bruno Rocha
7975f19817 Update querying.rst 2014-07-02 18:56:42 -03:00
Serge Chmelev
017602056d Add testcase 2014-07-03 01:48:19 +04:00
Serge Chmelev
c63f43854b Fix setting of _created flag in base.Document constructor 2014-07-03 01:48:19 +04:00
Serge Chmelev
5cc71ec2ad Fix for post_init signal to receive correct state of _created flag. 2014-07-03 01:48:19 +04:00
Omer Katz
80e81f8475 Python 2.6 requires positioning by number of fragments in format.
This commit will restore Python 2.6 compatability.
2014-07-02 23:22:06 +03:00
Omer Katz
3685c8e015 Allowed failures for Django development version. 2014-07-02 22:50:58 +03:00
Bruno Rocha
99e943c365 Updates with no operator should default to $set Fix #667 2014-07-02 14:39:29 -03:00
Yohan Graterol
21818e71f5 Revert change in .travis.yml: Delete apt cache 2014-06-30 08:48:04 -05:00
Ross Lawley
bcc6d25e21 Merge branch 'master' of github.com:MongoEngine/mongoengine 2014-06-30 10:30:12 +01:00
Ross Lawley
7b885ee0d3 Fix StrictDict repr 2014-06-30 10:29:28 +01:00
Omer Katz
c10e808a4f Fixed requirements file to fit the new PyMongo>=2.7.1 requirement. 2014-06-30 11:30:51 +03:00
Omer Katz
54e9be0ed8 Merge pull request #689 from brianhelba/pymongo_2.5
Make requirement for PyMongo>=2.5 more consistent
2014-06-30 11:30:15 +03:00
Yohan Graterol
938cdf316a Added cache for apt in Travis 2014-06-30 00:14:47 -05:00
Yohan Graterol
27c33911e6 Update .travis.yml 2014-06-30 00:09:28 -05:00
Yohan Graterol
e88f8759e7 Replace before_script for before_install 2014-06-29 23:33:30 -05:00
Yohan Graterol
f2992e3165 Travis problem with before_script 2014-06-29 23:31:08 -05:00
Yohan Graterol
c71fd1ee3b Before_script fixed. 2014-06-29 23:29:10 -05:00
Yohan Graterol
fb45b19fdc Enabling textSearch for build in Travis 2014-06-29 23:26:02 -05:00
Brian Helba
c4ea8d4942 Make requirement for PyMongo>=2.5 more consistent
Commit 7aa1f47378 requires PyMongo >= v2.5.
This updates the requirements file to make this requirement explicit to
package managers.

Commit 29309dac9a removed some legacy
compatibility code that would run only with versions of PyMongo < 2.1. The
options 'is_slave' and 'slaves' for register_connection were only used in
this compatibility code, so they are removed too.
2014-06-30 00:05:34 -04:00
Yohan Graterol
646aa131ef Corrected Travis config syntax 2014-06-29 22:58:10 -05:00
Yohan Graterol
0adb40bf92 Merge pull request #684 from brianhelba/doc-spelling
Fix some minor spelling and grammar in documentation
2014-06-29 22:41:23 -05:00
Brian Helba
17d6014bf1 Fix some minor spelling and grammar in documentation 2014-06-29 23:07:28 -04:00
Yohan Graterol
ff57cd4eaf Merge pull request #680 from claymation/text-index-specs
Include preliminary support for text indexes
2014-06-27 22:18:17 -05:00
Clay McClure
74bd7c3744 Include preliminary support for text indexes
To index a text field, prefix the field name with `$`, as in `$title`.
2014-06-27 14:48:32 -04:00
Omer Katz
cfbb283f85 Added Django 1.7RC1 to the build process and excluded it from running on Python 2.6. 2014-06-27 16:48:38 +03:00
Omer Katz
74a3c4451b using() was added in 0.9. Not 0.8. 2014-06-27 16:35:26 +03:00
Ross Lawley
be3643c962 Added elemMatch operator as well - match is too obscure #653 2014-06-27 13:39:47 +01:00
Ross Lawley
f4aa546af8 Added support for progressive JPEG #486 #548 2014-06-27 12:54:32 +01:00
Ross Lawley
67b876a7f4 Merge pull request #548 from yograterol/feature-progressive-jpeg
Feature for progressive JPEG. Issue #486
2014-06-27 12:53:51 +01:00
Ross Lawley
94e177c0ef Allow strings to be used in index creation #677 2014-06-27 12:49:31 +01:00
Ross Lawley
1bd83cc9bc Merge branch 'master' into pr/675 2014-06-27 12:48:28 +01:00
Ross Lawley
ecda3f4a7d Fixed EmbeddedDoc weakref proxy issue #592 2014-06-27 12:42:45 +01:00
Ross Lawley
8f972a965d Merge branch 'master' into pr/592 2014-06-27 12:36:39 +01:00
Ross Lawley
0f051fc57c Changelog & Authors #583 2014-06-27 12:33:19 +01:00
Ross Lawley
c3f8925f46 Merge pull request #583 from Gerrrr/distinct_bug
Fixed bug in distinct method
2014-06-27 12:22:09 +01:00
Ross Lawley
5d0cab2052 Merge branch 'master' into pr/539
Conflicts:
	mongoengine/base/datastructures.py
2014-06-27 12:20:44 +01:00
Ross Lawley
4d7492f682 Changelog & Author updates #425 #507 2014-06-27 12:10:17 +01:00
Ross Lawley
fc9d99080f Merge branch 'master' into pr/507
Conflicts:
	tests/document/dynamic.py
2014-06-27 12:06:18 +01:00
Ross Lawley
47ebac0276 Add authentication_source option to register_connection #178 #464 #573 #580 #590 2014-06-27 11:59:35 +01:00
Ross Lawley
cb3fca03e9 Merge branch 'master' into pr/590
Conflicts:
	mongoengine/connection.py
2014-06-27 11:53:46 +01:00
Ross Lawley
abbbd83729 Merge pull request #433 from reachveera/master
Overridden the prepare_query_value method in SequenceField inorder to re...
2014-06-27 11:49:28 +01:00
Ross Lawley
1743ab7812 Changelog update #567 2014-06-27 11:38:06 +01:00
Ross Lawley
324e3972a6 Merge pull request #567 from tomprimozic/master
Implemented equality between Documents and DBRefs
2014-06-27 11:37:24 +01:00
Ross Lawley
1502dda2ab Fixed ReferenceField inside nested ListFields dereferencing problem #368 2014-06-27 11:33:56 +01:00
Ross Lawley
f31b2c4a79 Merge branch 'master' into pr/368 2014-06-27 11:32:19 +01:00
Ross Lawley
89b9b60e0c Geo SON tweaks 2014-06-27 11:27:10 +01:00
Ross Lawley
de9ba12779 Turn on tests 2014-06-27 11:16:23 +01:00
Ross Lawley
9cc4359c04 Added the ability to reload specific document fields #100 2014-06-27 11:10:14 +01:00
Ross Lawley
67eaf120b9 db_alias support and fixes for custom map/reduce output #586 2014-06-27 10:07:05 +01:00
Ross Lawley
b8353c4a33 Merge branch 'master' into pr/586
Conflicts:
	mongoengine/queryset/base.py
	tests/queryset/queryset.py
2014-06-27 10:06:30 +01:00
Ross Lawley
7013033ae4 Update changelog & AUTHORS #594 #589 2014-06-27 10:03:35 +01:00
Ross Lawley
cb8cd03852 Merge pull request #594 from idlead/feature/post_save_delta
post_save signal should have delta information about field changes #594 #589
2014-06-27 10:02:08 +01:00
Ross Lawley
f63fb62014 Merge branch 'master' of github.com:MongoEngine/mongoengine 2014-06-27 10:00:42 +01:00
Ross Lawley
2e4fb86b86 Don't query with $orderby for qs.get() #600 2014-06-27 10:00:16 +01:00
Stefan Wojcik
5e776a07dd allow ordering to be cleared 2014-06-27 09:58:49 +01:00
Ross Lawley
81e637e50e Merge pull request #598 from philfreo/patch-1
clarifying the 'push' atomic update docs
2014-06-27 09:38:01 +01:00
Ross Lawley
0971ad0a80 Update changelog & authors - #636 2014-06-27 09:31:01 +01:00
Ross Lawley
8267ded7ec Merge branch 'master' into pr/636 2014-06-27 09:29:19 +01:00
Ross Lawley
7f36ea55f5 Fix bulk test where behaviour changes based on mongo version 2014-06-27 09:14:56 +01:00
Ross Lawley
72a051f2d3 Update AUTHORS & Changelog #557 2014-06-27 09:12:05 +01:00
Ross Lawley
51b197888c Merge remote-tracking branch 'origin/master' 2014-06-27 09:10:40 +01:00
Ross Lawley
cd63865d31 Fix clear_changed_fields() clearing unsaved documents bug #602 2014-06-27 09:08:07 +01:00
Martyn Smith
5be5685a09 Test to illustrate failure in changed attribute tracking 2014-06-27 09:06:17 +01:00
Yohan Graterol
76b2f25d46 Merge pull request #557 from SpotOnInc/recursive_embedded_errors_fix
Fixes issue with recursive embedded document errors
2014-06-26 21:55:49 -05:00
Ross Lawley
58607d4a7f Merge pull request #609 from nicolasdespres/fix-gridfs-guide
Save is called on the document not the file field.
2014-06-26 19:56:32 +01:00
Ross Lawley
c0a5b16a7f Travis bump 2014-06-26 19:52:05 +01:00
Ross Lawley
3a0c69005b Update AUTHORS and Changelog
Refs: #664, #677, #676, #673, #674, #655, #657, #626, #625, #619, #613, #608, #511, #559
2014-06-26 19:41:40 +01:00
Ross Lawley
5c295fb9e3 Merge branch 'master' of github.com:MongoEngine/mongoengine 2014-06-26 19:25:35 +01:00
Ross Lawley
4ee212e7d5 Skip Test due to server bug in 2.6 2014-06-26 19:25:05 +01:00
Ross Lawley
70651ce994 Fix as_pymongo bug 2014-06-26 19:24:52 +01:00
Yohan Graterol
a778a91106 Merge pull request #584 from FrankSomething/consistent-inits
inherit parent Document type _auto_id_field value
2014-06-26 11:24:13 -05:00
Ross Lawley
cfc31eead3 Fixed $maxDistance location for geoJSON $near queries with MongoDB 2.6+
Closes #664
2014-06-26 17:13:35 +01:00
Ross Lawley
da0a1bbe9f Fix test_using 2014-06-26 17:13:21 +01:00
Ross Lawley
bc66fb33e9 Merge branch 'master' into pr/625 2014-06-26 16:48:12 +01:00
Ross Lawley
b1b6493755 Merge branch 'pr/676' 2014-06-26 16:46:01 +01:00
Ross Lawley
1d189f239b Merge branch 'pr/562' 2014-06-26 16:42:23 +01:00
Ross Lawley
5b90691bcc Merge branch 'master' into pr/585 2014-06-26 16:41:27 +01:00
Ross Lawley
d1d5972277 Removed support for old versions
- Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x.
- Removing support for Python < 2.6.6
2014-06-26 16:34:02 +01:00
Ross Lawley
2c07d77368 Updated changelog
Enabled connection pooling
2014-06-26 16:24:37 +01:00
Ross Lawley
642cfbf59a Merge branch 'master' into pr/672
Conflicts:
	.travis.yml
2014-06-26 16:23:32 +01:00
Ross Lawley
bb1367cfb9 Merge branch 'master' into pr/674 2014-06-26 16:22:21 +01:00
Dmitry Konishchev
11724aa555 QuerySet.modify() method to provide find_and_modify() like behaviour 2014-06-26 16:18:42 +01:00
Ross Lawley
4d374712de Merge branch '0.9'
Conflicts:
	.travis.yml
2014-06-26 16:14:34 +01:00
Ross Lawley
eb9003187d Updated changelog & authors #673 2014-06-26 16:13:01 +01:00
Ross Lawley
caba444962 Merge branch '0.9' into pr/673
Conflicts:
	.travis.yml
2014-06-26 16:11:12 +01:00
Ross Lawley
5b6c8c191f Updated .travis.yml 2014-06-26 16:06:30 +01:00
Ross Lawley
dd51589f67 Updates 2014-06-26 16:02:40 +01:00
Ross Lawley
b02a31d4b9 Updated .travis.yml 2014-06-26 14:44:44 +01:00
Omer Katz
0e7878b406 Only run 2to3 on Python 3.x. Makes sense no? 2014-06-26 12:41:26 +03:00
Omer Katz
cae91ce0c5 Convert codebase to Python 3 using 2to3 before running benchmarks. 2014-06-26 12:31:07 +03:00
Omer Katz
67a65a2aa9 Installing unittest2 on Python 2.6. 2014-06-26 11:17:57 +03:00
Yohan Graterol
364b0a7163 Merge pull request #591 from pavlov99/master
fix docstring for DictField
2014-06-25 22:52:07 -05:00
Yohan Graterol
d6419f2059 Merge pull request #613 from falcondai/master
minor change to geo-related docs
2014-06-25 10:44:36 -05:00
Yohan Graterol
6f7ad7ef91 Merge pull request #619 from polyrabbit/master
Fixed incorrectly split a query key, when it ends with "_"
2014-06-25 10:42:27 -05:00
Omer Katz
5ae588833b Allowed to switch databases for a specific query. 2014-06-25 18:22:39 +03:00
Yohan Graterol
a70dbac0e6 Merge pull request #626 from KonishchevDmitry/pr-not-unique-error-on-update
Raise NotUniqueError in Document.update() on pymongo.errors.DuplicateKeyError
2014-06-25 10:18:09 -05:00
Yohan Graterol
4d34a02afe Merge pull request #634 from jatin/patch-1
Updated Jatin's name and github name
2014-06-25 10:16:49 -05:00
Yohan Graterol
4db4f45897 Merge pull request #651 from elasticsales/server-13975-precaution
Don't use a system collection in the tests
2014-06-25 09:48:30 -05:00
Yohan Graterol
2d5280fc95 Merge pull request #655 from jonathansp/master
Avoid to open all documents from cursors in an if stmt
2014-06-25 09:45:51 -05:00
Omer Katz
b8d568761e Getting rid of xrange since it's not in Python 3 and does not affect the benchmark. 2014-06-25 17:24:52 +03:00
Omer Katz
29309dac9a Mongo clients with the same settings should be shared since they manage a connection pool.
Also, I removed old code that was supposed to support Pymongo<2.1 which we don't support anymore.
2014-06-25 16:53:24 +03:00
Omer Katz
7f7745071a Found more print statements that were not turned into function calls. 2014-06-25 15:47:54 +03:00
Omer Katz
1914032e35 Missed some of the print statements in the benchmarks script. 2014-06-25 14:20:54 +03:00
Omer Katz
f44c8f1205 Skipping a test that does not work on PyPy due to a PyPy bug/feature. 2014-06-25 13:11:32 +03:00
Omer Katz
fe2ef4e61c Made the benchmark script compatitable with Python 3 and ensured it runs on every build. 2014-06-25 11:39:08 +03:00
Omer Katz
fc3eda55c7 Added a note about optional dependencies to the README file. 2014-06-25 11:32:41 +03:00
Omer Katz
8adf1cdd02 Fast finish the build if there are failures since we have a very large build matrix and each build takes a very long time. 2014-06-25 11:18:35 +03:00
Omer Katz
adbbc656d4 Removing zlib hack since only PIL needs it. The build should pass without it. 2014-06-25 11:12:40 +03:00
Omer Katz
8e852bce02 Pillow provides a more descriptive error message, therefor the build failure. 2014-06-25 10:58:00 +03:00
Omer Katz
bb461b009f Travis build improvements.
The latest patch version of each Django minor version is used.
The build now installs existing pymongo versions.
The build now actually tests against the specified Django version.
Replaced PIL with Pillow.
Added PyPy and Python 3.4 to the build.

Rebase Log:

Installing Pillow instead of PIL for testing since it's recommended and it supports PyPy.

Excluding Django versions that do not work with Python 3.

Improved formatting of .travis.yml.

Specifying Pillow 2.0.0 and above since it's the first version that is supported in Python 3.

PIL should not be installed alongside Pillow.

Also, I installed some libraries that both PIL and Pillow depend on.

It seems I have to be explicit on all envvars in order to exclude Django 1.4 from the build matrix.

The build is now installing pymongo versions that actually exist.

openjpeg has a different name on Ubuntu 12.04.

Restoring libz hack.

Also installing all Pillow requirements just in case.

Fixed the build matrix.

Acting according to @BanzaiMan's advice in travis-ci/travis-ci/#1492.
2014-06-25 10:40:28 +03:00
Omer Katz
03559a3cc4 Added Python 3.4 to the build process. 2014-06-24 19:20:15 +03:00
Jonathan Prates
7bb2fe128a Added PR #657 2014-06-12 11:08:41 -03:00
Jonathan Prates
2312e17a8e Merge remote-tracking branch 'elasticsales/clear-default-ordering' 2014-06-12 10:28:36 -03:00
Sagiv Malihi
9835b382da added __slots__ to BaseDocument and Document
changed the _data field to static key-value mapping instead of hash table
This implements #624
2014-06-10 16:11:27 +03:00
Stefan Wojcik
1eacc6fbff clear ordering via empty order_by 2014-05-30 15:08:03 -07:00
Jonathan Prates
85187239b6 Fix tests msg 2014-05-29 15:21:24 -03:00
Jonathan Prates
819ff2a902 Renamed to has_data() 2014-05-29 14:36:30 -03:00
Jonathan Prates
c744104a18 Added test with meta 2014-05-29 10:53:20 -03:00
Jonathan Prates
c87801f0a9 Using first() from cloned queryset 2014-05-28 17:26:28 -03:00
Jonathan Prates
39735594bd Removed blank line 2014-05-28 17:15:48 -03:00
Jonathan Prates
30964f65e4 Remove orderby in if stmt 2014-05-28 17:06:15 -03:00
Jonathan Prates
ee0c7fd8bf Change for loop to self.first() 2014-05-28 13:21:00 -03:00
Jonathan Prates
dfdecef8e7 Fix py2 and py3 2014-05-28 09:40:22 -03:00
Jonathan Prates
edcdfeb057 Fix syntax error 2014-05-28 09:03:12 -03:00
Jonathan Prates
47f0de9836 Py3 fix 2014-05-28 08:36:57 -03:00
Jonathan Prates
9ba657797e Authors updated according guideline 2014-05-28 08:33:22 -03:00
Clay McClure
07442a6f84 Allow index specs to be composed from raw strings
This allows an index spec to reference arbitrary keys of a DictField:

    class MyDoc(Document):
        frobs = DictField()
        meta = {
            "indexes": ["frobs.fmep", "frobs.gorp"],
        }
2014-05-28 01:31:35 -04:00
Jonathan Prates
3faf3c84be Avoid to open all documents from cursors in an if stmt
Using a cursos in an if statement:

cursor = Collection.objects

	if cursor:
		(...)

Will open all documents, because there are not an __nonzero__ method.
This change check only one document (if present) and returns True or False.
2014-05-27 16:33:38 -03:00
Stefan Wojcik
abcacc82f3 dont use a system collection 2014-05-21 22:21:46 -07:00
Ronald van Rij
9544b7d968 Fixed unit test which used assertIsNotNone 2014-05-09 14:33:18 +02:00
Ronald van Rij
babbc8bcd6 When using autogenerated document ids in a sharded collection, do set that id back into the Document 2014-05-06 09:34:16 +02:00
Jatin Chopra
12809ebc74 Updated Jatin's name and github name 2014-05-06 00:25:55 -07:00
Dmitry Konishchev
b45a601ad2 Test raising NotUniqueError by Document.update() 2014-04-15 19:32:42 +04:00
Serge Matveenko
f099dc6a37 Merge pull request #608 from cloudbuy/dateutil-bug-workaround
workaround a dateutil bug
2014-04-10 12:18:25 +04:00
Dmitry Konishchev
803caddbd4 Raise NotUniqueError in Document.update() on pymongo.errors.DuplicateKeyError 2014-04-09 14:25:53 +04:00
poly
4d7b988018 Fixed uncorrectly split a query key, when it ends with "_" 2014-04-01 19:52:21 +08:00
Falcon Dai
c1f88a4e14 minor change to geo-related docs 2014-03-17 22:29:53 -05:00
Nicolas Despres
5d9ec0b208 Save is called on the document not the file field. 2014-03-17 17:19:17 +01:00
Damien Churchill
1877cacf9c fix modifying slices under python3 2014-03-12 19:49:43 +00:00
Damien Churchill
2f4978cfea Merge branch 'dateutil-bug-workaround' 2014-03-12 17:27:04 +00:00
Damien Churchill
d27a1103fa workaround a dateutil bug
In the latest released version of dateutil, there's a bug whereby a TypeError
can be raised whilst parsing a date. This is because it calls a method which
it expects to return 2 arguments, however it can return 1 depending upon the
input, which results in a TypeError: ArgType not iterable exception. Since
this is equivalent to a failed parse anyway, we can treat it the same as a
ValueError.
2014-03-12 17:19:49 +00:00
Damien Churchill
b85bb95082 Merge branch 'master' of github.com:cloudbuy/mongoengine 2014-03-12 15:11:53 +00:00
Damien Churchill
db7f93cff3 improved update queries for BaseDict & BaseList
Migrate changes to include updating single elements of ListFields as
well as MapFields by adding the same changes to BaseList. This is
done by ensuring all BaseDicts and BaseLists have the correct name
from the base of the nearest (Embedded)Document, then marking changes
with their key or index when they are changed.

Tests also all fixed up.
2014-03-12 15:07:40 +00:00
Damien Churchill
85e271098f Merge branch 'master' of https://github.com/MongoEngine/mongoengine 2014-03-12 12:44:04 +00:00
Damien Churchill
17001e2f74 Merge remote-tracking branch 'origin/master' 2014-03-11 13:00:08 +00:00
Phil Freo
c82f4f0d45 clarifying the 'push' atomic update docs
the first time I read this I was all like... "no duh it will remove either the first or the last, but which does it do???"
2014-03-07 13:37:15 -08:00
tprimozi
88247a3af9 Bugfix for weakref _instance bug. 2014-03-03 15:11:05 +00:00
tprimozi
158578a406 Added test that fails due to weakref _instance bug. 2014-03-03 15:10:35 +00:00
Kirill Pavlov
19314e7e06 fix docstring for DictField 2014-03-03 13:09:26 +08:00
Brian Helba
8bcbc6d545 Add authentication_source option to register_connection (#573) (#580)
Since v2.5, PyMongo has supported a "source" option, to specify a
particular database to authenticate against. This adds support for that
option, in the form of a "authentication_source" option to
register_connection.
2014-03-02 18:35:49 -05:00
Vlad Zloteanu
ef55e6d476 fixes MongoEngine/mongoengine#589 2014-03-01 17:51:59 +01:00
Wilson Júnior
295ef3dc1d db_alias support and fixes for custom map/reduce output 2014-02-25 15:36:30 -03:00
Frank Battaglia
9d125c9e79 inherit parent Document type _auto_id_field value 2014-02-24 11:10:02 -05:00
Frank Battaglia
86363986fc whitespace 2014-02-24 11:04:29 -05:00
Frank Battaglia
0a2dbbc58b add tests for mongo query operators 2014-02-24 11:03:50 -05:00
Frank Battaglia
673a966541 add tests for save_condition kwarg in document.save() 2014-02-24 11:02:37 -05:00
Frank Battaglia
db1e69813b add atomic conditions to save
Conflicts:
	mongoengine/document.py
2014-02-24 10:57:32 -05:00
Aleksandr Sorokoumov
e60d56f060 test implemented 2014-02-24 19:22:36 +04:00
Aleksandr Sorokoumov
328e062ae9 Distinct method bugfix
Creation of instances is executed now only for EmbeddedDocumentField
and GenericEmbeddedDocumentField in distinct method
2014-02-24 19:21:11 +04:00
tprimozi
0523c2ea4b Fixed document equality: documents in different collections can have equal ids. 2014-02-13 18:12:33 +00:00
tprimozi
c5c7378c63 Implemented equality between Documents and DBRefs 2014-02-04 13:41:17 +00:00
Brian J. Dowling
9b2080d036 Added a test for allowing dynamic dictionary-style field access
Closes #559
2014-01-28 22:10:26 -05:00
Ross Lawley
d4b3649640 Added coveralls.io badge
https://coveralls.io/r/MongoEngine/mongoengine
2014-01-28 09:25:59 +00:00
Brian J. Dowling
b085993901 Allow dynamic dictionary-style field access
Allows the doc[key] syntax to work for dynamicembeddeddocument fields

Fixes #559
2014-01-27 23:05:29 +00:00
Andrei Zbikowski
0d4afad342 Fixes issue with recursive embedded document errors 2014-01-24 16:54:29 -06:00
Damien Churchill
eacb614750 Merge branch 'master' of https://github.com/MongoEngine/mongoengine 2014-01-10 11:03:48 +00:00
Yohan Graterol
341e1e7a6d Feature for progressive JPEG. Issue #486 2014-01-08 14:48:34 -04:30
Damien Churchill
2f6890c78a fix for nested MapFields
When using nested MapFields from a document loaded from the database, the
nested dictionaries aren't converted to BaseDict, so changes aren't
marked.

This also includes a change when marking a field as changed to ensure that
nested fields aren't included in a $set query if a parent is already marked
as changed. Not sure if this could occur but it prevents breakage if it does.
2013-12-16 13:44:07 +00:00
Eric Plumb
857cd718df Fix for issue #425 - allow undeclared fields in an embedded dynamic document to be seen by queryset methods 2013-11-08 14:57:35 -08:00
veera
c9dc441915 Overridden the prepare_query_value method in SequenceField inorder to return the value as the required type. 2013-08-05 15:33:54 +05:30
Stefan Wojcik
a7ca9950fc potential fix for dereferencing nested lists 2013-06-11 15:36:35 -07:00
Stefan Wojcik
e0dd33e6be move the test into a more appropriate location 2013-06-11 12:18:03 -07:00
Stefan Wojcik
2e718e1130 unit test showing the problem 2013-06-11 12:00:59 -07:00
59 changed files with 5368 additions and 913 deletions

View File

@@ -1,33 +1,72 @@
# http://travis-ci.org/#!/MongoEngine/mongoengine
language: python language: python
services: mongodb
python: python:
- "2.6" - '2.6'
- "2.7" - '2.7'
- "3.2" - '3.2'
- "3.3" - '3.3'
- '3.4'
- pypy
- pypy3
env: env:
- PYMONGO=dev DJANGO=1.6 - PYMONGO=2.7.2 DJANGO=dev
- PYMONGO=dev DJANGO=1.5.5 - PYMONGO=2.7.2 DJANGO=1.7.1
- PYMONGO=dev DJANGO=1.4.10 - PYMONGO=2.7.2 DJANGO=1.6.8
- PYMONGO=2.5 DJANGO=1.6 - PYMONGO=2.7.2 DJANGO=1.5.11
- PYMONGO=2.5 DJANGO=1.5.5 - PYMONGO=2.8 DJANGO=dev
- PYMONGO=2.5 DJANGO=1.4.10 - PYMONGO=2.8 DJANGO=1.7.1
- PYMONGO=3.2 DJANGO=1.6 - PYMONGO=2.8 DJANGO=1.6.8
- PYMONGO=3.2 DJANGO=1.5.5 - PYMONGO=2.8 DJANGO=1.5.11
- PYMONGO=3.3 DJANGO=1.6 matrix:
- PYMONGO=3.3 DJANGO=1.5.5 exclude:
- python: '2.6'
env: PYMONGO=2.7.2 DJANGO=dev
- python: '2.6'
env: PYMONGO=2.8 DJANGO=dev
- python: '2.6'
env: PYMONGO=2.7.2 DJANGO=1.7.1
- python: '2.6'
env: PYMONGO=2.8 DJANGO=1.7.1
allow_failures:
- python: pypy3
fast_finish: true
before_install:
- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
- echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' |
sudo tee /etc/apt/sources.list.d/mongodb.list
- travis_retry sudo apt-get update
- travis_retry sudo apt-get install mongodb-org-server
install: install:
- if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
- if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev
- if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi python-tk
- if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi - if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master;
- pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b true; fi
- python setup.py install - if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true;
fi
- if [[ $DJANGO == 'dev' ]]; then travis_retry pip install git+https://github.com/django/django.git;
fi
- if [[ $DJANGO != 'dev' ]]; then travis_retry pip install Django==$DJANGO; fi
- travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b
- travis_retry pip install coveralls
- travis_retry python setup.py install
script: script:
- python setup.py test - travis_retry python setup.py test
- if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then 2to3 . -w; fi;
- coverage run --source=mongoengine setup.py test
- coverage report -m
- python benchmark.py
after_script: coveralls --verbose
notifications: notifications:
irc: "irc.freenode.org#mongoengine" irc: irc.freenode.org#mongoengine
branches: branches:
only: only:
- master - master
- /^v.*$/
deploy:
provider: pypi
user: the_drow
password:
secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek=
on:
tags: true
repo: MongoEngine/mongoengine

33
AUTHORS
View File

@@ -142,7 +142,7 @@ that much better:
* Pete Campton * Pete Campton
* Martyn Smith * Martyn Smith
* Marcelo Anton * Marcelo Anton
* Aleksey Porfirov * Aleksey Porfirov (https://github.com/lexqt)
* Nicolas Trippar * Nicolas Trippar
* Manuel Hermann * Manuel Hermann
* Gustavo Gawryszewski * Gustavo Gawryszewski
@@ -171,7 +171,7 @@ that much better:
* Michael Bartnett (https://github.com/michaelbartnett) * Michael Bartnett (https://github.com/michaelbartnett)
* Alon Horev (https://github.com/alonho) * Alon Horev (https://github.com/alonho)
* Kelvin Hammond (https://github.com/kelvinhammond) * Kelvin Hammond (https://github.com/kelvinhammond)
* Jatin- (https://github.com/jatin-) * Jatin Chopra (https://github.com/jatin)
* Paul Uithol (https://github.com/PaulUithol) * Paul Uithol (https://github.com/PaulUithol)
* Thom Knowles (https://github.com/fleat) * Thom Knowles (https://github.com/fleat)
* Paul (https://github.com/squamous) * Paul (https://github.com/squamous)
@@ -189,3 +189,32 @@ that much better:
* Tom (https://github.com/tomprimozic) * Tom (https://github.com/tomprimozic)
* j0hnsmith (https://github.com/j0hnsmith) * j0hnsmith (https://github.com/j0hnsmith)
* Damien Churchill (https://github.com/damoxc) * Damien Churchill (https://github.com/damoxc)
* Jonathan Simon Prates (https://github.com/jonathansp)
* Thiago Papageorgiou (https://github.com/tmpapageorgiou)
* Omer Katz (https://github.com/thedrow)
* Falcon Dai (https://github.com/falcondai)
* Polyrabbit (https://github.com/polyrabbit)
* Sagiv Malihi (https://github.com/sagivmalihi)
* Dmitry Konishchev (https://github.com/KonishchevDmitry)
* Martyn Smith (https://github.com/martynsmith)
* Andrei Zbikowski (https://github.com/b1naryth1ef)
* Ronald van Rij (https://github.com/ronaldvanrij)
* François Schmidts (https://github.com/jaesivsm)
* Eric Plumb (https://github.com/professorplumb)
* Damien Churchill (https://github.com/damoxc)
* Aleksandr Sorokoumov (https://github.com/Gerrrr)
* Clay McClure (https://github.com/claymation)
* Bruno Rocha (https://github.com/rochacbruno)
* Norberto Leite (https://github.com/nleite)
* Bob Cribbs (https://github.com/bocribbz)
* Jay Shirley (https://github.com/jshirley)
* David Bordeynik (https://github.com/DavidBord)
* Axel Haustant (https://github.com/noirbizarre)
* David Czarnecki (https://github.com/czarneckid)
* Vyacheslav Murashkin (https://github.com/a4tunado)
* André Ericson https://github.com/aericson)
* Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky)
* Diego Berrocal (https://github.com/cestdiego)
* Matthew Ellison (https://github.com/seglberg)
* Jimmy Shen (https://github.com/jimmyshen)
* J. Fernando Sánchez (https://github.com/balkian)

View File

@@ -9,6 +9,13 @@ MongoEngine
.. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master .. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master
:target: http://travis-ci.org/MongoEngine/mongoengine :target: http://travis-ci.org/MongoEngine/mongoengine
.. image:: https://coveralls.io/repos/MongoEngine/mongoengine/badge.png?branch=master
:target: https://coveralls.io/r/MongoEngine/mongoengine?branch=master
.. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.png
:target: https://landscape.io/github/MongoEngine/mongoengine/master
:alt: Code Health
About About
===== =====
MongoEngine is a Python Object-Document Mapper for working with MongoDB. MongoEngine is a Python Object-Document Mapper for working with MongoDB.
@@ -26,9 +33,18 @@ setup.py install``.
Dependencies Dependencies
============ ============
- pymongo 2.5+ - pymongo>=2.7.1
- sphinx (optional - for documentation generation) - sphinx (optional - for documentation generation)
Optional Dependencies
---------------------
- **Django Integration:** Django>=1.4.0 for Python 2.x or PyPy and Django>=1.5.0 for Python 3.x
- **Image Fields**: Pillow>=2.0.0 or PIL (not recommended since MongoEngine is tested with Pillow)
- dateutil>=2.1.0
.. note
MongoEngine always runs it's test suite against the latest patch version of each dependecy. e.g.: Django 1.6.5
Examples Examples
======== ========
Some simple examples of what MongoEngine code looks like:: Some simple examples of what MongoEngine code looks like::

View File

@@ -15,7 +15,7 @@ def cprofile_main():
class Noddy(Document): class Noddy(Document):
fields = DictField() fields = DictField()
for i in xrange(1): for i in range(1):
noddy = Noddy() noddy = Noddy()
for j in range(20): for j in range(20):
noddy.fields["key" + str(j)] = "value " + str(j) noddy.fields["key" + str(j)] = "value " + str(j)
@@ -113,6 +113,7 @@ def main():
4.68946313858 4.68946313858
---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------
""" """
print("Benchmarking...")
setup = """ setup = """
from pymongo import MongoClient from pymongo import MongoClient
@@ -127,7 +128,7 @@ connection = MongoClient()
db = connection.timeit_test db = connection.timeit_test
noddy = db.noddy noddy = db.noddy
for i in xrange(10000): for i in range(10000):
example = {'fields': {}} example = {'fields': {}}
for j in range(20): for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j) example['fields']["key"+str(j)] = "value "+str(j)
@@ -138,10 +139,10 @@ myNoddys = noddy.find()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries - Pymongo""" print("""Creating 10000 dictionaries - Pymongo""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
stmt = """ stmt = """
from pymongo import MongoClient from pymongo import MongoClient
@@ -150,7 +151,7 @@ connection = MongoClient()
db = connection.timeit_test db = connection.timeit_test
noddy = db.noddy noddy = db.noddy
for i in xrange(10000): for i in range(10000):
example = {'fields': {}} example = {'fields': {}}
for j in range(20): for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j) example['fields']["key"+str(j)] = "value "+str(j)
@@ -161,10 +162,10 @@ myNoddys = noddy.find()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""" print("""Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
setup = """ setup = """
from pymongo import MongoClient from pymongo import MongoClient
@@ -180,7 +181,7 @@ class Noddy(Document):
""" """
stmt = """ stmt = """
for i in xrange(10000): for i in range(10000):
noddy = Noddy() noddy = Noddy()
for j in range(20): for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j) noddy.fields["key"+str(j)] = "value "+str(j)
@@ -190,13 +191,13 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries - MongoEngine""" print("""Creating 10000 dictionaries - MongoEngine""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
stmt = """ stmt = """
for i in xrange(10000): for i in range(10000):
noddy = Noddy() noddy = Noddy()
fields = {} fields = {}
for j in range(20): for j in range(20):
@@ -208,13 +209,13 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries without continual assign - MongoEngine""" print("""Creating 10000 dictionaries without continual assign - MongoEngine""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
stmt = """ stmt = """
for i in xrange(10000): for i in range(10000):
noddy = Noddy() noddy = Noddy()
for j in range(20): for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j) noddy.fields["key"+str(j)] = "value "+str(j)
@@ -224,13 +225,13 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""" print("""Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
stmt = """ stmt = """
for i in xrange(10000): for i in range(10000):
noddy = Noddy() noddy = Noddy()
for j in range(20): for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j) noddy.fields["key"+str(j)] = "value "+str(j)
@@ -240,13 +241,13 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""" print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
stmt = """ stmt = """
for i in xrange(10000): for i in range(10000):
noddy = Noddy() noddy = Noddy()
for j in range(20): for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j) noddy.fields["key"+str(j)] = "value "+str(j)
@@ -256,13 +257,13 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False""" print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
stmt = """ stmt = """
for i in xrange(10000): for i in range(10000):
noddy = Noddy() noddy = Noddy()
for j in range(20): for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j) noddy.fields["key"+str(j)] = "value "+str(j)
@@ -272,10 +273,10 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print "-" * 100 print("-" * 100)
print """Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False""" print("""Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False""")
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print t.timeit(1) print(t.timeit(1))
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -79,11 +79,13 @@ Fields
.. autoclass:: mongoengine.fields.GenericEmbeddedDocumentField .. autoclass:: mongoengine.fields.GenericEmbeddedDocumentField
.. autoclass:: mongoengine.fields.DynamicField .. autoclass:: mongoengine.fields.DynamicField
.. autoclass:: mongoengine.fields.ListField .. autoclass:: mongoengine.fields.ListField
.. autoclass:: mongoengine.fields.EmbeddedDocumentListField
.. autoclass:: mongoengine.fields.SortedListField .. autoclass:: mongoengine.fields.SortedListField
.. autoclass:: mongoengine.fields.DictField .. autoclass:: mongoengine.fields.DictField
.. autoclass:: mongoengine.fields.MapField .. autoclass:: mongoengine.fields.MapField
.. autoclass:: mongoengine.fields.ReferenceField .. autoclass:: mongoengine.fields.ReferenceField
.. autoclass:: mongoengine.fields.GenericReferenceField .. autoclass:: mongoengine.fields.GenericReferenceField
.. autoclass:: mongoengine.fields.CachedReferenceField
.. autoclass:: mongoengine.fields.BinaryField .. autoclass:: mongoengine.fields.BinaryField
.. autoclass:: mongoengine.fields.FileField .. autoclass:: mongoengine.fields.FileField
.. autoclass:: mongoengine.fields.ImageField .. autoclass:: mongoengine.fields.ImageField
@@ -94,11 +96,29 @@ Fields
.. autoclass:: mongoengine.fields.PointField .. autoclass:: mongoengine.fields.PointField
.. autoclass:: mongoengine.fields.LineStringField .. autoclass:: mongoengine.fields.LineStringField
.. autoclass:: mongoengine.fields.PolygonField .. autoclass:: mongoengine.fields.PolygonField
.. autoclass:: mongoengine.fields.MultiPointField
.. autoclass:: mongoengine.fields.MultiLineStringField
.. autoclass:: mongoengine.fields.MultiPolygonField
.. autoclass:: mongoengine.fields.GridFSError .. autoclass:: mongoengine.fields.GridFSError
.. autoclass:: mongoengine.fields.GridFSProxy .. autoclass:: mongoengine.fields.GridFSProxy
.. autoclass:: mongoengine.fields.ImageGridFsProxy .. autoclass:: mongoengine.fields.ImageGridFsProxy
.. autoclass:: mongoengine.fields.ImproperlyConfigured .. autoclass:: mongoengine.fields.ImproperlyConfigured
Embedded Document Querying
==========================
.. versionadded:: 0.9
Additional queries for Embedded Documents are available when using the
:class:`~mongoengine.EmbeddedDocumentListField` to store a list of embedded
documents.
A list of embedded documents is returned as a special list with the
following methods:
.. autoclass:: mongoengine.base.datastructures.EmbeddedDocumentList
:members:
Misc Misc
==== ====

View File

@@ -2,9 +2,86 @@
Changelog Changelog
========= =========
Changes in 0.9.X - DEV
======================
- Update FileField when creating a new file #714
- Added `EmbeddedDocumentListField` for Lists of Embedded Documents. #826
- ComplexDateTimeField should fall back to None when null=True #864
- Request Support for $min, $max Field update operators #863
- `BaseDict` does not follow `setdefault` #866
- Add support for $type operator # 766
- Fix tests for pymongo 2.8+ #877
- No module named 'django.utils.importlib' (Django dev) #872
- Field Choices Now Accept Subclasses of Documents
- Ensure Indexes before Each Save #812
- Generate Unique Indices for Lists of EmbeddedDocuments #358
- Sparse fields #515
- write_concern not in params of Collection#remove #801
- Better BaseDocument equality check when not saved #798
- OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771
- with_limit_and_skip for count should default like in pymongo #759
- Fix storing value of precision attribute in DecimalField #787
- Set attribute to None does not work (at least for fields with default values) #734
- Querying by a field defined in a subclass raises InvalidQueryError #744
- Add Support For MongoDB 2.6.X's maxTimeMS #778
- abstract shouldn't be inherited in EmbeddedDocument # 789
- Allow specifying the '_cls' as a field for indexes #397
- Stop ensure_indexes running on a secondaries unless connection is through mongos #746
- Not overriding default values when loading a subset of fields #399
- Saving document doesn't create new fields in existing collection #620
- Added `Queryset.aggregate` wrapper to aggregation framework #703
- Added support to show original model fields on to_json calls instead of db_field #697
- Added Queryset.search_text to Text indexes searchs #700
- Fixed tests for Django 1.7 #696
- Follow ReferenceFields in EmbeddedDocuments with select_related #690
- Added preliminary support for text indexes #680
- Added `elemMatch` operator as well - `match` is too obscure #653
- Added support for progressive JPEG #486 #548
- Allow strings to be used in index creation #675
- Fixed EmbeddedDoc weakref proxy issue #592
- Fixed nested reference field distinct error #583
- Fixed change tracking on nested MapFields #539
- Dynamic fields in embedded documents now visible to queryset.only() / qs.exclude() #425 #507
- Add authentication_source option to register_connection #178 #464 #573 #580 #590
- Implemented equality between Documents and DBRefs #597
- Fixed ReferenceField inside nested ListFields dereferencing problem #368
- Added the ability to reload specific document fields #100
- Added db_alias support and fixes for custom map/reduce output #586
- post_save signal now has access to delta information about field changes #594 #589
- Don't query with $orderby for qs.get() #600
- Fix id shard key save issue #636
- Fixes issue with recursive embedded document errors #557
- Fix clear_changed_fields() clearing unsaved documents bug #602
- Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x.
- Removing support for Python < 2.6.6
- Fixed $maxDistance location for geoJSON $near queries with MongoDB 2.6+ #664
- QuerySet.modify() and Document.modify() methods to provide find_and_modify() like behaviour #677 #773
- Added support for the using() method on a queryset #676
- PYPY support #673
- Connection pooling #674
- Avoid to open all documents from cursors in an if stmt #655
- Ability to clear the ordering #657
- Raise NotUniqueError in Document.update() on pymongo.errors.DuplicateKeyError #626
- Slots - memory improvements #625
- Fixed incorrectly split a query key when it ends with "_" #619
- Geo docs updates #613
- Workaround a dateutil bug #608
- Conditional save for atomic-style operations #511
- Allow dynamic dictionary-style field access #559
- Increase email field length to accommodate new TLDs #726
- index_cls is ignored when deciding to set _cls as index prefix #733
- Make 'db' argument to connection optional #737
- Allow atomic update for the entire `DictField` #742
- Added MultiPointField, MultiLineField, MultiPolygonField
- Fix multiple connections aliases being rewritten #748
- Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791
- Make `in_bulk()` respect `no_dereference()` #775
- Handle None from model __str__; Fixes #753 #754
Changes in 0.8.7 Changes in 0.8.7
================ ================
- Calling reload on deleted / nonexistant documents raises DoesNotExist (#538) - Calling reload on deleted / nonexistent documents raises DoesNotExist (#538)
- Stop ensure_indexes running on a secondaries (#555) - Stop ensure_indexes running on a secondaries (#555)
- Fix circular import issue with django auth (#531) (#545) - Fix circular import issue with django auth (#531) (#545)
@@ -17,7 +94,7 @@ Changes in 0.8.5
- Fix multi level nested fields getting marked as changed (#523) - Fix multi level nested fields getting marked as changed (#523)
- Django 1.6 login fix (#522) (#527) - Django 1.6 login fix (#522) (#527)
- Django 1.6 session fix (#509) - Django 1.6 session fix (#509)
- EmbeddedDocument._instance is now set when settng the attribute (#506) - EmbeddedDocument._instance is now set when setting the attribute (#506)
- Fixed EmbeddedDocument with ReferenceField equality issue (#502) - Fixed EmbeddedDocument with ReferenceField equality issue (#502)
- Fixed GenericReferenceField serialization order (#499) - Fixed GenericReferenceField serialization order (#499)
- Fixed count and none bug (#498) - Fixed count and none bug (#498)
@@ -107,7 +184,7 @@ Changes in 0.8.0
- Added `get_next_value` preview for SequenceFields (#319) - Added `get_next_value` preview for SequenceFields (#319)
- Added no_sub_classes context manager and queryset helper (#312) - Added no_sub_classes context manager and queryset helper (#312)
- Querysets now utilises a local cache - Querysets now utilises a local cache
- Changed __len__ behavour in the queryset (#247, #311) - Changed __len__ behaviour in the queryset (#247, #311)
- Fixed querying string versions of ObjectIds issue with ReferenceField (#307) - Fixed querying string versions of ObjectIds issue with ReferenceField (#307)
- Added $setOnInsert support for upserts (#308) - Added $setOnInsert support for upserts (#308)
- Upserts now possible with just query parameters (#309) - Upserts now possible with just query parameters (#309)
@@ -158,7 +235,7 @@ Changes in 0.8.0
- Uses getlasterror to test created on updated saves (#163) - Uses getlasterror to test created on updated saves (#163)
- Fixed inheritance and unique index creation (#140) - Fixed inheritance and unique index creation (#140)
- Fixed reverse delete rule with inheritance (#197) - Fixed reverse delete rule with inheritance (#197)
- Fixed validation for GenericReferences which havent been dereferenced - Fixed validation for GenericReferences which haven't been dereferenced
- Added switch_db context manager (#106) - Added switch_db context manager (#106)
- Added switch_db method to document instances (#106) - Added switch_db method to document instances (#106)
- Added no_dereference context manager (#82) (#61) - Added no_dereference context manager (#82) (#61)
@@ -240,11 +317,11 @@ Changes in 0.7.2
- Update index spec generation so its not destructive (#113) - Update index spec generation so its not destructive (#113)
Changes in 0.7.1 Changes in 0.7.1
================= ================
- Fixed index spec inheritance (#111) - Fixed index spec inheritance (#111)
Changes in 0.7.0 Changes in 0.7.0
================= ================
- Updated queryset.delete so you can use with skip / limit (#107) - Updated queryset.delete so you can use with skip / limit (#107)
- Updated index creation allows kwargs to be passed through refs (#104) - Updated index creation allows kwargs to be passed through refs (#104)
- Fixed Q object merge edge case (#109) - Fixed Q object merge edge case (#109)
@@ -325,7 +402,7 @@ Changes in 0.6.12
- Fixes error with _delta handling DBRefs - Fixes error with _delta handling DBRefs
Changes in 0.6.11 Changes in 0.6.11
================== =================
- Fixed inconsistency handling None values field attrs - Fixed inconsistency handling None values field attrs
- Fixed map_field embedded db_field issue - Fixed map_field embedded db_field issue
- Fixed .save() _delta issue with DbRefs - Fixed .save() _delta issue with DbRefs
@@ -405,7 +482,7 @@ Changes in 0.6.1
- Fix for replicaSet connections - Fix for replicaSet connections
Changes in 0.6 Changes in 0.6
================ ==============
- Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7 - 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 - Added support for covered indexes when inheritance is off
@@ -493,8 +570,8 @@ Changes in v0.5
- Updated default collection naming convention - Updated default collection naming convention
- Added Document Mixin support - Added Document Mixin support
- Fixed queryet __repr__ mid iteration - Fixed queryet __repr__ mid iteration
- Added hint() support, so cantell Mongo the proper index to use for the query - Added hint() support, so can tell Mongo the proper index to use for the query
- Fixed issue with inconsitent setting of _cls breaking inherited referencing - Fixed issue with inconsistent setting of _cls breaking inherited referencing
- Added help_text and verbose_name to fields to help with some form libs - Added help_text and verbose_name to fields to help with some form libs
- Updated item_frequencies to handle embedded document lookups - Updated item_frequencies to handle embedded document lookups
- Added delta tracking now only sets / unsets explicitly changed fields - Added delta tracking now only sets / unsets explicitly changed fields

View File

@@ -23,21 +23,32 @@ arguments should be provided::
connect('project1', username='webapp', password='pwd123') connect('project1', username='webapp', password='pwd123')
Uri style connections are also supported - just supply the uri as URI style connections are also supported -- just supply the URI as
the :attr:`host` to the :attr:`host` to
:func:`~mongoengine.connect`:: :func:`~mongoengine.connect`::
connect('project1', host='mongodb://localhost/database_name') connect('project1', host='mongodb://localhost/database_name')
Note that database name from uri has priority over name .. note:: Database, username and password from URI string overrides
in ::func:`~mongoengine.connect` corresponding parameters in :func:`~mongoengine.connect`: ::
connect(
name='test',
username='user',
password='12345',
host='mongodb://admin:qwerty@localhost/production'
)
will establish connection to ``production`` database using
``admin`` username and ``qwerty`` password.
ReplicaSets ReplicaSets
=========== ===========
MongoEngine supports :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient` MongoEngine supports
to use them please use a URI style connection and provide the `replicaSet` name in the :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. To use them,
connection kwargs. please use an URI style connection and provide the ``replicaSet`` name
in the connection kwargs.
Read preferences are supported through the connection or via individual Read preferences are supported through the connection or via individual
queries by passing the read_preference :: queries by passing the read_preference ::
@@ -77,16 +88,19 @@ to point across databases and collections. Below is an example schema, using
meta = {"db_alias": "users-books-db"} meta = {"db_alias": "users-books-db"}
Switch Database Context Manager Context Managers
=============================== ================
Sometimes you may want to switch the database or collection to query against
Sometimes you may want to switch the database to query against for a class for a class.
for example, archiving older data into a separate database for performance For example, archiving older data into a separate database for performance
reasons. reasons or writing functions that dynamically choose collections to write
document to.
Switch Database
---------------
The :class:`~mongoengine.context_managers.switch_db` context manager allows The :class:`~mongoengine.context_managers.switch_db` context manager allows
you to change the database alias for a given class allowing quick and easy you to change the database alias for a given class allowing quick and easy
access to the same User document across databases:: access the same User document across databases::
from mongoengine.context_managers import switch_db from mongoengine.context_managers import switch_db
@@ -98,15 +112,14 @@ access to the same User document across databases::
with switch_db(User, 'archive-user-db') as User: with switch_db(User, 'archive-user-db') as User:
User(name="Ross").save() # Saves the 'archive-user-db' User(name="Ross").save() # Saves the 'archive-user-db'
.. note:: Make sure any aliases have been registered with
:func:`~mongoengine.register_connection` before using the context manager.
There is also a switch collection context manager as well. The Switch Collection
:class:`~mongoengine.context_managers.switch_collection` context manager allows -----------------
you to change the collection for a given class allowing quick and easy The :class:`~mongoengine.context_managers.switch_collection` context manager
access to the same Group document across collection:: allows you to change the collection for a given class allowing quick and easy
access the same Group document across collection::
from mongoengine.context_managers import switch_db from mongoengine.context_managers import switch_collection
class Group(Document): class Group(Document):
name = StringField() name = StringField()
@@ -115,3 +128,9 @@ access to the same Group document across collection::
with switch_collection(Group, 'group2000') as Group: with switch_collection(Group, 'group2000') as Group:
Group(name="hello Group 2000 collection!").save() # Saves in group2000 collection Group(name="hello Group 2000 collection!").save() # Saves in group2000 collection
.. note:: Make sure any aliases have been registered with
:func:`~mongoengine.register_connection` or :func:`~mongoengine.connect`
before using the context manager.

View File

@@ -4,7 +4,7 @@ Defining documents
In MongoDB, a **document** is roughly equivalent to a **row** in an RDBMS. When In MongoDB, a **document** is roughly equivalent to a **row** in an RDBMS. When
working with relational databases, rows are stored in **tables**, which have a working with relational databases, rows are stored in **tables**, which have a
strict **schema** that the rows follow. MongoDB stores documents in strict **schema** that the rows follow. MongoDB stores documents in
**collections** rather than tables - the principle difference is that no schema **collections** rather than tables --- the principal difference is that no schema
is enforced at a database level. is enforced at a database level.
Defining a document's schema Defining a document's schema
@@ -91,6 +91,12 @@ are as follows:
* :class:`~mongoengine.fields.StringField` * :class:`~mongoengine.fields.StringField`
* :class:`~mongoengine.fields.URLField` * :class:`~mongoengine.fields.URLField`
* :class:`~mongoengine.fields.UUIDField` * :class:`~mongoengine.fields.UUIDField`
* :class:`~mongoengine.fields.PointField`
* :class:`~mongoengine.fields.LineStringField`
* :class:`~mongoengine.fields.PolygonField`
* :class:`~mongoengine.fields.MultiPointField`
* :class:`~mongoengine.fields.MultiLineStringField`
* :class:`~mongoengine.fields.MultiPolygonField`
Field arguments Field arguments
--------------- ---------------
@@ -165,15 +171,15 @@ arguments can be set on all fields:
size = StringField(max_length=3, choices=SIZE) size = StringField(max_length=3, choices=SIZE)
:attr:`help_text` (Default: None) :attr:`help_text` (Default: None)
Optional help text to output with the field - used by form libraries Optional help text to output with the field -- used by form libraries
:attr:`verbose_name` (Default: None) :attr:`verbose_name` (Default: None)
Optional human-readable name for the field - used by form libraries Optional human-readable name for the field -- used by form libraries
List fields List fields
----------- -----------
MongoDB allows the storage of lists of items. To add a list of items to a MongoDB allows storing lists of items. To add a list of items to a
:class:`~mongoengine.Document`, use the :class:`~mongoengine.fields.ListField` field :class:`~mongoengine.Document`, use the :class:`~mongoengine.fields.ListField` field
type. :class:`~mongoengine.fields.ListField` takes another field object as its first type. :class:`~mongoengine.fields.ListField` takes another field object as its first
argument, which specifies which type elements may be stored within the list:: argument, which specifies which type elements may be stored within the list::
@@ -328,7 +334,7 @@ Its value can take any of the following constants:
Any object's fields still referring to the object being deleted are removed Any object's fields still referring to the object being deleted are removed
(using MongoDB's "unset" operation), effectively nullifying the relationship. (using MongoDB's "unset" operation), effectively nullifying the relationship.
:const:`mongoengine.CASCADE` :const:`mongoengine.CASCADE`
Any object containing fields that are refererring to the object being deleted Any object containing fields that are referring to the object being deleted
are deleted first. are deleted first.
:const:`mongoengine.PULL` :const:`mongoengine.PULL`
Removes the reference to the object (using MongoDB's "pull" operation) Removes the reference to the object (using MongoDB's "pull" operation)
@@ -422,7 +428,7 @@ Document collections
==================== ====================
Document classes that inherit **directly** from :class:`~mongoengine.Document` Document classes that inherit **directly** from :class:`~mongoengine.Document`
will have their own **collection** in the database. The name of the collection will have their own **collection** in the database. The name of the collection
is by default the name of the class, coverted to lowercase (so in the example is by default the name of the class, converted to lowercase (so in the example
above, the collection would be called `page`). If you need to change the name above, the collection would be called `page`). If you need to change the name
of the collection (e.g. to use MongoEngine with an existing database), then of the collection (e.g. to use MongoEngine with an existing database), then
create a class dictionary attribute called :attr:`meta` on your document, and create a class dictionary attribute called :attr:`meta` on your document, and
@@ -459,13 +465,22 @@ by creating a list of index specifications called :attr:`indexes` in the
either be a single field name, a tuple containing multiple field names, or a either be a single field name, a tuple containing multiple field names, or a
dictionary containing a full index definition. A direction may be specified on dictionary containing a full index definition. A direction may be specified on
fields by prefixing the field name with a **+** (for ascending) or a **-** sign fields by prefixing the field name with a **+** (for ascending) or a **-** sign
(for descending). Note that direction only matters on multi-field indexes. :: (for descending). Note that direction only matters on multi-field indexes.
Text indexes may be specified by prefixing the field name with a **$**. ::
class Page(Document): class Page(Document):
title = StringField() title = StringField()
rating = StringField() rating = StringField()
created = DateTimeField()
meta = { meta = {
'indexes': ['title', ('title', '-rating')] 'indexes': [
'title',
('title', '-rating'),
{
'fields': ['created'],
'expireAfterSeconds': 3600
}
]
} }
If a dictionary is passed then the following options are available: If a dictionary is passed then the following options are available:
@@ -531,6 +546,8 @@ field name to the index definition.
Sometimes its more efficient to index parts of Embedded / dictionary fields, Sometimes its more efficient to index parts of Embedded / dictionary fields,
in this case use 'dot' notation to identify the value to index eg: `rank.title` in this case use 'dot' notation to identify the value to index eg: `rank.title`
.. _geospatial-indexes:
Geospatial indexes Geospatial indexes
------------------ ------------------
@@ -541,6 +558,9 @@ The following fields will explicitly add a "2dsphere" index:
- :class:`~mongoengine.fields.PointField` - :class:`~mongoengine.fields.PointField`
- :class:`~mongoengine.fields.LineStringField` - :class:`~mongoengine.fields.LineStringField`
- :class:`~mongoengine.fields.PolygonField` - :class:`~mongoengine.fields.PolygonField`
- :class:`~mongoengine.fields.MultiPointField`
- :class:`~mongoengine.fields.MultiLineStringField`
- :class:`~mongoengine.fields.MultiPolygonField`
As "2dsphere" indexes can be part of a compound index, you may not want the As "2dsphere" indexes can be part of a compound index, you may not want the
automatic index but would prefer a compound index. In this example we turn off automatic index but would prefer a compound index. In this example we turn off
@@ -652,11 +672,11 @@ Shard keys
========== ==========
If your collection is sharded, then you need to specify the shard key as a tuple, 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`. 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 This ensures that the shard key is sent with the query when calling the
:meth:`~mongoengine.document.Document.save` or :meth:`~mongoengine.document.Document.save` or
:meth:`~mongoengine.document.Document.update` method on an existing :meth:`~mongoengine.document.Document.update` method on an existing
:class:`-mongoengine.Document` instance:: :class:`~mongoengine.Document` instance::
class LogEntry(Document): class LogEntry(Document):
machine = StringField() machine = StringField()
@@ -678,7 +698,7 @@ 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 As this is new class is not a direct subclass of
:class:`~mongoengine.Document`, it will not be stored in its own collection; it :class:`~mongoengine.Document`, it will not be stored in its own collection; it
will use the same collection as its superclass uses. This allows for more will use the same collection as its superclass uses. This allows for more
convenient and efficient retrieval of related documents - all you need do is convenient and efficient retrieval of related documents -- all you need do is
set :attr:`allow_inheritance` to True in the :attr:`meta` data for a set :attr:`allow_inheritance` to True in the :attr:`meta` data for a
document.:: document.::
@@ -692,12 +712,12 @@ document.::
class DatedPage(Page): class DatedPage(Page):
date = DateTimeField() date = DateTimeField()
.. note:: From 0.8 onwards you must declare :attr:`allow_inheritance` defaults .. note:: From 0.8 onwards :attr:`allow_inheritance` defaults
to False, meaning you must set it to True to use inheritance. to False, meaning you must set it to True to use inheritance.
Working with existing data Working with existing data
-------------------------- --------------------------
As MongoEngine no longer defaults to needing :attr:`_cls` you can quickly and As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and
easily get working with existing data. Just define the document to match easily get working with existing data. Just define the document to match
the expected schema in your database :: the expected schema in your database ::
@@ -720,7 +740,7 @@ Abstract classes
If you want to add some extra functionality to a group of Document classes but If you want to add some extra functionality to a group of Document classes but
you don't need or want the overhead of inheritance you can use the you don't need or want the overhead of inheritance you can use the
:attr:`abstract` attribute of :attr:`-mongoengine.Document.meta`. :attr:`abstract` attribute of :attr:`~mongoengine.Document.meta`.
This won't turn on :ref:`document-inheritance` but will allow you to keep your This won't turn on :ref:`document-inheritance` but will allow you to keep your
code DRY:: code DRY::

View File

@@ -2,7 +2,7 @@
Documents instances Documents instances
=================== ===================
To create a new document object, create an instance of the relevant document To create a new document object, create an instance of the relevant document
class, providing values for its fields as its constructor keyword arguments. class, providing values for its fields as constructor keyword arguments.
You may provide values for any of the fields on the document:: You may provide values for any of the fields on the document::
>>> page = Page(title="Test Page") >>> page = Page(title="Test Page")
@@ -32,11 +32,11 @@ already exist, then any changes will be updated atomically. For example::
Changes to documents are tracked and on the whole perform ``set`` operations. Changes to documents are tracked and on the whole perform ``set`` operations.
* ``list_field.push(0)`` - *sets* the resulting list * ``list_field.push(0)`` --- *sets* the resulting list
* ``del(list_field)`` - *unsets* whole list * ``del(list_field)`` --- *unsets* whole list
With lists its preferable to use ``Doc.update(push__list_field=0)`` as With lists its preferable to use ``Doc.update(push__list_field=0)`` as
this stops the whole list being updated - stopping any race conditions. this stops the whole list being updated --- stopping any race conditions.
.. seealso:: .. seealso::
:ref:`guide-atomic-updates` :ref:`guide-atomic-updates`
@@ -74,7 +74,7 @@ Cascading Saves
If your document contains :class:`~mongoengine.fields.ReferenceField` or If your document contains :class:`~mongoengine.fields.ReferenceField` or
:class:`~mongoengine.fields.GenericReferenceField` objects, then by default the :class:`~mongoengine.fields.GenericReferenceField` objects, then by default the
:meth:`~mongoengine.Document.save` method will not save any changes to :meth:`~mongoengine.Document.save` method will not save any changes to
those objects. If you want all references to also be saved also, noting each those objects. If you want all references to be saved also, noting each
save is a separate query, then passing :attr:`cascade` as True save is a separate query, then passing :attr:`cascade` as True
to the save method will cascade any saves. to the save method will cascade any saves.
@@ -113,12 +113,13 @@ you may still use :attr:`id` to access the primary key if you want::
>>> bob.id == bob.email == 'bob@example.com' >>> bob.id == bob.email == 'bob@example.com'
True True
You can also access the document's "primary key" using the :attr:`pk` field; in You can also access the document's "primary key" using the :attr:`pk` field,
is an alias to :attr:`id`:: it's an alias to :attr:`id`::
>>> page = Page(title="Another Test Page") >>> page = Page(title="Another Test Page")
>>> page.save() >>> page.save()
>>> page.id == page.pk >>> page.id == page.pk
True
.. note:: .. note::

View File

@@ -46,7 +46,7 @@ slightly different manner. First, a new file must be created by calling the
marmot.photo.write('some_more_image_data') marmot.photo.write('some_more_image_data')
marmot.photo.close() marmot.photo.close()
marmot.photo.save() marmot.save()
Deletion Deletion
-------- --------

View File

@@ -12,3 +12,4 @@ User Guide
querying querying
gridfs gridfs
signals signals
text-indexes

View File

@@ -17,7 +17,7 @@ fetch documents from the database::
As of MongoEngine 0.8 the querysets utilise a local cache. So iterating As of MongoEngine 0.8 the querysets utilise a local cache. So iterating
it multiple times will only cause a single query. If this is not the it multiple times will only cause a single query. If this is not the
desired behavour you can call :class:`~mongoengine.QuerySet.no_cache` desired behaviour you can call :class:`~mongoengine.QuerySet.no_cache`
(version **0.8.3+**) to return a non-caching queryset. (version **0.8.3+**) to return a non-caching queryset.
Filtering queries Filtering queries
@@ -42,7 +42,7 @@ syntax::
Query operators Query operators
=============== ===============
Operators other than equality may also be used in queries; just attach the Operators other than equality may also be used in queries --- just attach the
operator name to a key with a double-underscore:: operator name to a key with a double-underscore::
# Only find users whose age is 18 or less # Only find users whose age is 18 or less
@@ -84,19 +84,20 @@ expressions:
Geo queries Geo queries
----------- -----------
There are a few special operators for performing geographical queries. The following There are a few special operators for performing geographical queries.
were added in 0.8 for: :class:`~mongoengine.fields.PointField`, The following were added in MongoEngine 0.8 for
:class:`~mongoengine.fields.PointField`,
:class:`~mongoengine.fields.LineStringField` and :class:`~mongoengine.fields.LineStringField` and
:class:`~mongoengine.fields.PolygonField`: :class:`~mongoengine.fields.PolygonField`:
* ``geo_within`` -- Check if a geometry is within a polygon. For ease of use * ``geo_within`` -- check if a geometry is within a polygon. For ease of use
it accepts either a geojson geometry or just the polygon coordinates eg:: it accepts either a geojson geometry or just the polygon coordinates eg::
loc.objects(point__geo_within=[[[40, 5], [40, 6], [41, 6], [40, 5]]]) loc.objects(point__geo_within=[[[40, 5], [40, 6], [41, 6], [40, 5]]])
loc.objects(point__geo_within={"type": "Polygon", loc.objects(point__geo_within={"type": "Polygon",
"coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]})
* ``geo_within_box`` - simplified geo_within searching with a box eg:: * ``geo_within_box`` -- simplified geo_within searching with a box eg::
loc.objects(point__geo_within_box=[(-125.0, 35.0), (-100.0, 40.0)]) loc.objects(point__geo_within_box=[(-125.0, 35.0), (-100.0, 40.0)])
loc.objects(point__geo_within_box=[<bottom left coordinates>, <upper right coordinates>]) loc.objects(point__geo_within_box=[<bottom left coordinates>, <upper right coordinates>])
@@ -132,23 +133,21 @@ were added in 0.8 for: :class:`~mongoengine.fields.PointField`,
loc.objects(poly__geo_intersects={"type": "Polygon", loc.objects(poly__geo_intersects={"type": "Polygon",
"coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}) "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]})
* ``near`` -- Find all the locations near a given point:: * ``near`` -- find all the locations near a given point::
loc.objects(point__near=[40, 5]) loc.objects(point__near=[40, 5])
loc.objects(point__near={"type": "Point", "coordinates": [40, 5]}) loc.objects(point__near={"type": "Point", "coordinates": [40, 5]})
You can also set the maximum distance in meters as well:: You can also set the maximum distance in meters as well::
loc.objects(point__near=[40, 5], point__max_distance=1000) loc.objects(point__near=[40, 5], point__max_distance=1000)
The older 2D indexes are still supported with the The older 2D indexes are still supported with the
:class:`~mongoengine.fields.GeoPointField`: :class:`~mongoengine.fields.GeoPointField`:
* ``within_distance`` -- provide a list containing a point and a maximum * ``within_distance`` -- provide a list containing a point and a maximum
distance (e.g. [(41.342, -87.653), 5]) distance (e.g. [(41.342, -87.653), 5])
* ``within_spherical_distance`` -- Same as above but using the spherical geo model * ``within_spherical_distance`` -- same as above but using the spherical geo model
(e.g. [(41.342, -87.653), 5/earth_radius]) (e.g. [(41.342, -87.653), 5/earth_radius])
* ``near`` -- order the documents by how close they are to a given point * ``near`` -- order the documents by how close they are to a given point
* ``near_sphere`` -- Same as above but using the spherical geo model * ``near_sphere`` -- Same as above but using the spherical geo model
@@ -198,12 +197,14 @@ However, this doesn't map well to the syntax so you can also use a capital S ins
Post.objects(comments__by="joe").update(inc__comments__S__votes=1) Post.objects(comments__by="joe").update(inc__comments__S__votes=1)
.. note:: Due to Mongo currently the $ operator only applies to the first matched item in the query. .. note::
Due to :program:`Mongo`, currently the $ operator only applies to the
first matched item in the query.
Raw queries Raw queries
----------- -----------
It is possible to provide a raw PyMongo query as a query parameter, which will It is possible to provide a raw :mod:`PyMongo` query as a query parameter, which will
be integrated directly into the query. This is done using the ``__raw__`` be integrated directly into the query. This is done using the ``__raw__``
keyword argument:: keyword argument::
@@ -213,12 +214,12 @@ keyword argument::
Limiting and skipping results Limiting and skipping results
============================= =============================
Just as with traditional ORMs, you may limit the number of results returned, or Just as with traditional ORMs, you may limit the number of results returned or
skip a number or results in you query. skip a number or results in you query.
:meth:`~mongoengine.queryset.QuerySet.limit` and :meth:`~mongoengine.queryset.QuerySet.limit` and
:meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on :meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on
:class:`~mongoengine.queryset.QuerySet` objects, but the prefered syntax for :class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax
achieving this is using array-slicing syntax:: is preferred for achieving this::
# Only the first 5 people # Only the first 5 people
users = User.objects[:5] users = User.objects[:5]
@@ -252,10 +253,10 @@ To retrieve a result that should be unique in the collection, use
no document matches the query, and no document matches the query, and
:class:`~mongoengine.queryset.MultipleObjectsReturned` :class:`~mongoengine.queryset.MultipleObjectsReturned`
if more than one document matched the query. These exceptions are merged into if more than one document matched the query. These exceptions are merged into
your document defintions eg: `MyDoc.DoesNotExist` your document definitions eg: `MyDoc.DoesNotExist`
A variation of this method exists, A variation of this method exists,
:meth:`~mongoengine.queryset.Queryset.get_or_create`, that will create a new :meth:`~mongoengine.queryset.QuerySet.get_or_create`, that will create a new
document with the query arguments if no documents match the query. An document with the query arguments if no documents match the query. An
additional keyword argument, :attr:`defaults` may be provided, which will be additional keyword argument, :attr:`defaults` may be provided, which will be
used as default values for the new document, in the case that it should need used as default values for the new document, in the case that it should need
@@ -266,9 +267,13 @@ to be created::
>>> a.name == b.name and a.age == b.age >>> a.name == b.name and a.age == b.age
True True
.. warning::
:meth:`~mongoengine.queryset.QuerySet.get_or_create` method is deprecated
since :mod:`mongoengine` 0.8.
Default Document queries Default Document queries
======================== ========================
By default, the objects :attr:`~mongoengine.Document.objects` attribute on a By default, the objects :attr:`~Document.objects` attribute on a
document returns a :class:`~mongoengine.queryset.QuerySet` that doesn't filter document returns a :class:`~mongoengine.queryset.QuerySet` that doesn't filter
the collection -- it returns all objects. This may be changed by defining a the collection -- it returns all objects. This may be changed by defining a
method on a document that modifies a queryset. The method should accept two method on a document that modifies a queryset. The method should accept two
@@ -311,7 +316,7 @@ Should you want to add custom methods for interacting with or filtering
documents, extending the :class:`~mongoengine.queryset.QuerySet` class may be documents, extending the :class:`~mongoengine.queryset.QuerySet` class may be
the way to go. To use a custom :class:`~mongoengine.queryset.QuerySet` class on the way to go. To use a custom :class:`~mongoengine.queryset.QuerySet` class on
a document, set ``queryset_class`` to the custom class in a a document, set ``queryset_class`` to the custom class in a
:class:`~mongoengine.Document`\ s ``meta`` dictionary:: :class:`~mongoengine.Document`'s ``meta`` dictionary::
class AwesomerQuerySet(QuerySet): class AwesomerQuerySet(QuerySet):
@@ -488,22 +493,28 @@ calling it with keyword arguments::
Atomic updates Atomic updates
============== ==============
Documents may be updated atomically by using the Documents may be updated atomically by using the
:meth:`~mongoengine.queryset.QuerySet.update_one` and :meth:`~mongoengine.queryset.QuerySet.update_one`,
:meth:`~mongoengine.queryset.QuerySet.update` methods on a :meth:`~mongoengine.queryset.QuerySet.update` and
:meth:`~mongoengine.queryset.QuerySet`. There are several different "modifiers" :meth:`~mongoengine.queryset.QuerySet.modify` methods on a
that you may use with these methods: :class:`~mongoengine.queryset.QuerySet` or
:meth:`~mongoengine.Document.modify` and
:meth:`~mongoengine.Document.save` (with :attr:`save_condition` argument) on a
:class:`~mongoengine.Document`.
There are several different "modifiers" that you may use with these methods:
* ``set`` -- set a particular value * ``set`` -- set a particular value
* ``unset`` -- delete a particular value (since MongoDB v1.3+) * ``unset`` -- delete a particular value (since MongoDB v1.3)
* ``inc`` -- increment a value by a given amount * ``inc`` -- increment a value by a given amount
* ``dec`` -- decrement a value by a given amount * ``dec`` -- decrement a value by a given amount
* ``push`` -- append a value to a list * ``push`` -- append a value to a list
* ``push_all`` -- append several values to a list * ``push_all`` -- append several values to a list
* ``pop`` -- remove the first or last element of a list * ``pop`` -- remove the first or last element of a list `depending on the value`_
* ``pull`` -- remove a value from a list * ``pull`` -- remove a value from a list
* ``pull_all`` -- remove several values from a list * ``pull_all`` -- remove several values from a list
* ``add_to_set`` -- add value to a list only if its not in the list already * ``add_to_set`` -- add value to a list only if its not in the list already
.. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/
The syntax for atomic updates is similar to the querying syntax, but the The syntax for atomic updates is similar to the querying syntax, but the
modifier comes before the field, not after it:: modifier comes before the field, not after it::
@@ -522,6 +533,13 @@ modifier comes before the field, not after it::
>>> post.tags >>> post.tags
['database', 'nosql'] ['database', 'nosql']
.. note::
If no modifier operator is specified the default will be ``$set``. So the following sentences are identical::
>>> BlogPost.objects(id=post.id).update(title='Example Post')
>>> BlogPost.objects(id=post.id).update(set__title='Example Post')
.. note:: .. note::
In version 0.5 the :meth:`~mongoengine.Document.save` runs atomic updates In version 0.5 the :meth:`~mongoengine.Document.save` runs atomic updates
@@ -645,3 +663,4 @@ following example shows how the substitutions are made::
return comments; return comments;
} }
""") """)

View File

@@ -35,25 +35,25 @@ Available signals include:
:class:`~mongoengine.EmbeddedDocument` instance has been completed. :class:`~mongoengine.EmbeddedDocument` instance has been completed.
`pre_save` `pre_save`
Called within :meth:`~mongoengine.document.Document.save` prior to performing Called within :meth:`~mongoengine.Document.save` prior to performing
any actions. any actions.
`pre_save_post_validation` `pre_save_post_validation`
Called within :meth:`~mongoengine.document.Document.save` after validation Called within :meth:`~mongoengine.Document.save` after validation
has taken place but before saving. has taken place but before saving.
`post_save` `post_save`
Called within :meth:`~mongoengine.document.Document.save` after all actions Called within :meth:`~mongoengine.Document.save` after all actions
(validation, insert/update, cascades, clearing dirty flags) have completed (validation, insert/update, cascades, clearing dirty flags) have completed
successfully. Passed the additional boolean keyword argument `created` to successfully. Passed the additional boolean keyword argument `created` to
indicate if the save was an insert or an update. indicate if the save was an insert or an update.
`pre_delete` `pre_delete`
Called within :meth:`~mongoengine.document.Document.delete` prior to Called within :meth:`~mongoengine.Document.delete` prior to
attempting the delete operation. attempting the delete operation.
`post_delete` `post_delete`
Called within :meth:`~mongoengine.document.Document.delete` upon successful Called within :meth:`~mongoengine.Document.delete` upon successful
deletion of the record. deletion of the record.
`pre_bulk_insert` `pre_bulk_insert`
@@ -145,7 +145,7 @@ cleaner looking while still allowing manual execution of the callback::
ReferenceFields and Signals ReferenceFields and Signals
--------------------------- ---------------------------
Currently `reverse_delete_rules` do not trigger signals on the other part of Currently `reverse_delete_rule` does not trigger signals on the other part of
the relationship. If this is required you must manually handle the the relationship. If this is required you must manually handle the
reverse deletion. reverse deletion.

View File

@@ -0,0 +1,51 @@
===========
Text Search
===========
After MongoDB 2.4 version, supports search documents by text indexes.
Defining a Document with text index
===================================
Use the *$* prefix to set a text index, Look the declaration::
class News(Document):
title = StringField()
content = StringField()
is_active = BooleanField()
meta = {'indexes': [
{'fields': ['$title', "$content"],
'default_language': 'english',
'weight': {'title': 10, 'content': 2}
}
]}
Querying
========
Saving a document::
News(title="Using mongodb text search",
content="Testing text search").save()
News(title="MongoEngine 0.9 released",
content="Various improvements").save()
Next, start a text search using :attr:`QuerySet.search_text` method::
document = News.objects.search_text('testing').first()
document.title # may be: "Using mongodb text search"
document = News.objects.search_text('released').first()
document.title # may be: "MongoEngine 0.9 released"
Ordering by text score
======================
::
objects = News.objects.search('mongo').order_by('$text_score')

View File

@@ -14,7 +14,7 @@ MongoDB. To install it, simply run
MongoEngine. MongoEngine.
:doc:`guide/index` :doc:`guide/index`
The Full guide to MongoEngine - from modeling documents to storing files, The Full guide to MongoEngine --- from modeling documents to storing files,
from querying for data to firing signals and *everything* between. from querying for data to firing signals and *everything* between.
:doc:`apireference` :doc:`apireference`

View File

@@ -65,7 +65,7 @@ which fields a :class:`User` may have, and what types of data they might store::
first_name = StringField(max_length=50) first_name = StringField(max_length=50)
last_name = StringField(max_length=50) last_name = StringField(max_length=50)
This looks similar to how a the structure of a table would be defined in a This looks similar to how the structure of a table would be defined in a
regular ORM. The key difference is that this schema will never be passed on to regular ORM. The key difference is that this schema will never be passed on to
MongoDB --- this will only be enforced at the application level, making future MongoDB --- this will only be enforced at the application level, making future
changes easy to manage. Also, the User documents will be stored in a changes easy to manage. Also, the User documents will be stored in a

View File

@@ -5,7 +5,7 @@ Upgrading
0.8.7 0.8.7
***** *****
Calling reload on deleted / nonexistant documents now raises a DoesNotExist Calling reload on deleted / nonexistent documents now raises a DoesNotExist
exception. exception.
@@ -263,7 +263,7 @@ update your code like so: ::
[m for m in mammals] # This will return all carnivores [m for m in mammals] # This will return all carnivores
Len iterates the queryset Len iterates the queryset
-------------------------- -------------------------
If you ever did `len(queryset)` it previously did a `count()` under the covers, If you ever did `len(queryset)` it previously did a `count()` under the covers,
this caused some unusual issues. As `len(queryset)` is most often used by this caused some unusual issues. As `len(queryset)` is most often used by

View File

@@ -15,7 +15,7 @@ import django
__all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ +
list(queryset.__all__) + signals.__all__ + list(errors.__all__)) list(queryset.__all__) + signals.__all__ + list(errors.__all__))
VERSION = (0, 8, 7) VERSION = (0, 9, 0)
def get_version(): def get_version():

View File

@@ -1,12 +1,14 @@
import weakref import weakref
import functools
import itertools
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
__all__ = ("BaseDict", "BaseList") __all__ = ("BaseDict", "BaseList", "EmbeddedDocumentList")
class BaseDict(dict): class BaseDict(dict):
"""A special dict so we can watch any changes """A special dict so we can watch any changes"""
"""
_dereferenced = False _dereferenced = False
_instance = None _instance = None
@@ -21,29 +23,37 @@ class BaseDict(dict):
self._name = name self._name = name
return super(BaseDict, self).__init__(dict_items) return super(BaseDict, self).__init__(dict_items)
def __getitem__(self, *args, **kwargs): def __getitem__(self, key, *args, **kwargs):
value = super(BaseDict, self).__getitem__(*args, **kwargs) value = super(BaseDict, self).__getitem__(key)
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
if isinstance(value, EmbeddedDocument) and value._instance is None: if isinstance(value, EmbeddedDocument) and value._instance is None:
value._instance = self._instance value._instance = self._instance
elif not isinstance(value, BaseDict) and isinstance(value, dict):
value = BaseDict(value, None, '%s.%s' % (self._name, key))
super(BaseDict, self).__setitem__(key, value)
value._instance = self._instance
elif not isinstance(value, BaseList) and isinstance(value, list):
value = BaseList(value, None, '%s.%s' % (self._name, key))
super(BaseDict, self).__setitem__(key, value)
value._instance = self._instance
return value return value
def __setitem__(self, *args, **kwargs): def __setitem__(self, key, value, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed(key)
return super(BaseDict, self).__setitem__(*args, **kwargs) return super(BaseDict, self).__setitem__(key, value)
def __delete__(self, *args, **kwargs): def __delete__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
return super(BaseDict, self).__delete__(*args, **kwargs) return super(BaseDict, self).__delete__(*args, **kwargs)
def __delitem__(self, *args, **kwargs): def __delitem__(self, key, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed(key)
return super(BaseDict, self).__delitem__(*args, **kwargs) return super(BaseDict, self).__delitem__(key)
def __delattr__(self, *args, **kwargs): def __delattr__(self, key, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed(key)
return super(BaseDict, self).__delattr__(*args, **kwargs) return super(BaseDict, self).__delattr__(key)
def __getstate__(self): def __getstate__(self):
self.instance = None self.instance = None
@@ -66,12 +76,19 @@ class BaseDict(dict):
self._mark_as_changed() self._mark_as_changed()
return super(BaseDict, self).popitem(*args, **kwargs) return super(BaseDict, self).popitem(*args, **kwargs)
def setdefault(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseDict, self).setdefault(*args, **kwargs)
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
return super(BaseDict, self).update(*args, **kwargs) return super(BaseDict, self).update(*args, **kwargs)
def _mark_as_changed(self): def _mark_as_changed(self, key=None):
if hasattr(self._instance, '_mark_as_changed'): if hasattr(self._instance, '_mark_as_changed'):
if key:
self._instance._mark_as_changed('%s.%s' % (self._name, key))
else:
self._instance._mark_as_changed(self._name) self._instance._mark_as_changed(self._name)
@@ -90,23 +107,37 @@ class BaseList(list):
if isinstance(instance, (Document, EmbeddedDocument)): if isinstance(instance, (Document, EmbeddedDocument)):
self._instance = weakref.proxy(instance) self._instance = weakref.proxy(instance)
self._name = name self._name = name
return super(BaseList, self).__init__(list_items) super(BaseList, self).__init__(list_items)
def __getitem__(self, *args, **kwargs): def __getitem__(self, key, *args, **kwargs):
value = super(BaseList, self).__getitem__(*args, **kwargs) value = super(BaseList, self).__getitem__(key)
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
if isinstance(value, EmbeddedDocument) and value._instance is None: if isinstance(value, EmbeddedDocument) and value._instance is None:
value._instance = self._instance value._instance = self._instance
elif not isinstance(value, BaseDict) and isinstance(value, dict):
value = BaseDict(value, None, '%s.%s' % (self._name, key))
super(BaseList, self).__setitem__(key, value)
value._instance = self._instance
elif not isinstance(value, BaseList) and isinstance(value, list):
value = BaseList(value, None, '%s.%s' % (self._name, key))
super(BaseList, self).__setitem__(key, value)
value._instance = self._instance
return value return value
def __setitem__(self, *args, **kwargs): def __setitem__(self, key, value, *args, **kwargs):
if isinstance(key, slice):
self._mark_as_changed() self._mark_as_changed()
return super(BaseList, self).__setitem__(*args, **kwargs) else:
self._mark_as_changed(key)
return super(BaseList, self).__setitem__(key, value)
def __delitem__(self, *args, **kwargs): def __delitem__(self, key, *args, **kwargs):
if isinstance(key, slice):
self._mark_as_changed() self._mark_as_changed()
return super(BaseList, self).__delitem__(*args, **kwargs) else:
self._mark_as_changed(key)
return super(BaseList, self).__delitem__(key)
def __setslice__(self, *args, **kwargs): def __setslice__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
@@ -153,6 +184,266 @@ class BaseList(list):
self._mark_as_changed() self._mark_as_changed()
return super(BaseList, self).sort(*args, **kwargs) return super(BaseList, self).sort(*args, **kwargs)
def _mark_as_changed(self): def _mark_as_changed(self, key=None):
if hasattr(self._instance, '_mark_as_changed'): if hasattr(self._instance, '_mark_as_changed'):
if key:
self._instance._mark_as_changed('%s.%s' % (self._name, key))
else:
self._instance._mark_as_changed(self._name) self._instance._mark_as_changed(self._name)
class EmbeddedDocumentList(BaseList):
@classmethod
def __match_all(cls, i, kwargs):
items = kwargs.items()
return all([
getattr(i, k) == v or str(getattr(i, k)) == v for k, v in items
])
@classmethod
def __only_matches(cls, obj, kwargs):
if not kwargs:
return obj
return filter(lambda i: cls.__match_all(i, kwargs), obj)
def __init__(self, list_items, instance, name):
super(EmbeddedDocumentList, self).__init__(list_items, instance, name)
self._instance = instance
def filter(self, **kwargs):
"""
Filters the list by only including embedded documents with the
given keyword arguments.
:param kwargs: The keyword arguments corresponding to the fields to
filter on. *Multiple arguments are treated as if they are ANDed
together.*
:return: A new ``EmbeddedDocumentList`` containing the matching
embedded documents.
Raises ``AttributeError`` if a given keyword is not a valid field for
the embedded document class.
"""
values = self.__only_matches(self, kwargs)
return EmbeddedDocumentList(values, self._instance, self._name)
def exclude(self, **kwargs):
"""
Filters the list by excluding embedded documents with the given
keyword arguments.
:param kwargs: The keyword arguments corresponding to the fields to
exclude on. *Multiple arguments are treated as if they are ANDed
together.*
:return: A new ``EmbeddedDocumentList`` containing the non-matching
embedded documents.
Raises ``AttributeError`` if a given keyword is not a valid field for
the embedded document class.
"""
exclude = self.__only_matches(self, kwargs)
values = [item for item in self if item not in exclude]
return EmbeddedDocumentList(values, self._instance, self._name)
def count(self):
"""
The number of embedded documents in the list.
:return: The length of the list, equivalent to the result of ``len()``.
"""
return len(self)
def get(self, **kwargs):
"""
Retrieves an embedded document determined by the given keyword
arguments.
:param kwargs: The keyword arguments corresponding to the fields to
search on. *Multiple arguments are treated as if they are ANDed
together.*
:return: The embedded document matched by the given keyword arguments.
Raises ``DoesNotExist`` if the arguments used to query an embedded
document returns no results. ``MultipleObjectsReturned`` if more
than one result is returned.
"""
values = self.__only_matches(self, kwargs)
if len(values) == 0:
raise DoesNotExist(
"%s matching query does not exist." % self._name
)
elif len(values) > 1:
raise MultipleObjectsReturned(
"%d items returned, instead of 1" % len(values)
)
return values[0]
def first(self):
"""
Returns the first embedded document in the list, or ``None`` if empty.
"""
if len(self) > 0:
return self[0]
def create(self, **values):
"""
Creates a new embedded document and saves it to the database.
.. note::
The embedded document changes are not automatically saved
to the database after calling this method.
:param values: A dictionary of values for the embedded document.
:return: The new embedded document instance.
"""
name = self._name
EmbeddedClass = self._instance._fields[name].field.document_type_obj
self._instance[self._name].append(EmbeddedClass(**values))
return self._instance[self._name][-1]
def save(self, *args, **kwargs):
"""
Saves the ancestor document.
:param args: Arguments passed up to the ancestor Document's save
method.
:param kwargs: Keyword arguments passed up to the ancestor Document's
save method.
"""
self._instance.save(*args, **kwargs)
def delete(self):
"""
Deletes the embedded documents from the database.
.. note::
The embedded document changes are not automatically saved
to the database after calling this method.
:return: The number of entries deleted.
"""
values = list(self)
for item in values:
self._instance[self._name].remove(item)
return len(values)
def update(self, **update):
"""
Updates the embedded documents with the given update values.
.. note::
The embedded document changes are not automatically saved
to the database after calling this method.
:param update: A dictionary of update values to apply to each
embedded document.
:return: The number of entries updated.
"""
if len(update) == 0:
return 0
values = list(self)
for item in values:
for k, v in update.items():
setattr(item, k, v)
return len(values)
class StrictDict(object):
__slots__ = ()
_special_fields = set(['get', 'pop', 'iteritems', 'items', 'keys', 'create'])
_classes = {}
def __init__(self, **kwargs):
for k,v in kwargs.iteritems():
setattr(self, k, v)
def __getitem__(self, key):
key = '_reserved_' + key if key in self._special_fields else key
try:
return getattr(self, key)
except AttributeError:
raise KeyError(key)
def __setitem__(self, key, value):
key = '_reserved_' + key if key in self._special_fields else key
return setattr(self, key, value)
def __contains__(self, key):
return hasattr(self, key)
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def pop(self, key, default=None):
v = self.get(key, default)
try:
delattr(self, key)
except AttributeError:
pass
return v
def iteritems(self):
for key in self:
yield key, self[key]
def items(self):
return [(k, self[k]) for k in iter(self)]
def keys(self):
return list(iter(self))
def __iter__(self):
return (key for key in self.__slots__ if hasattr(self, key))
def __len__(self):
return len(list(self.iteritems()))
def __eq__(self, other):
return self.items() == other.items()
def __neq__(self, other):
return self.items() != other.items()
@classmethod
def create(cls, allowed_keys):
allowed_keys_tuple = tuple(('_reserved_' + k if k in cls._special_fields else k) for k in allowed_keys)
allowed_keys = frozenset(allowed_keys_tuple)
if allowed_keys not in cls._classes:
class SpecificStrictDict(cls):
__slots__ = allowed_keys_tuple
def __repr__(self):
return "{%s}" % ', '.join('"{0!s}": {0!r}'.format(k,v) for (k,v) in self.iteritems())
cls._classes[allowed_keys] = SpecificStrictDict
return cls._classes[allowed_keys]
class SemiStrictDict(StrictDict):
__slots__ = ('_extras')
_classes = {}
def __getattr__(self, attr):
try:
super(SemiStrictDict, self).__getattr__(attr)
except AttributeError:
try:
return self.__getattribute__('_extras')[attr]
except KeyError as e:
raise AttributeError(e)
def __setattr__(self, attr, value):
try:
super(SemiStrictDict, self).__setattr__(attr, value)
except AttributeError:
try:
self._extras[attr] = value
except AttributeError:
self._extras = {attr: value}
def __delattr__(self, attr):
try:
super(SemiStrictDict, self).__delattr__(attr)
except AttributeError:
try:
del self._extras[attr]
except KeyError as e:
raise AttributeError(e)
def __iter__(self):
try:
extras_iter = iter(self.__getattribute__('_extras'))
except AttributeError:
extras_iter = ()
return itertools.chain(super(SemiStrictDict, self).__iter__(), extras_iter)

View File

@@ -12,12 +12,17 @@ from bson.son import SON
from mongoengine import signals from mongoengine import signals
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.errors import (ValidationError, InvalidDocumentError, from mongoengine.errors import (ValidationError, InvalidDocumentError,
LookUpError) LookUpError, FieldDoesNotExist)
from mongoengine.python_support import (PY3, UNICODE_KWARGS, txt_type, from mongoengine.python_support import PY3, txt_type
to_str_keys_recursive)
from mongoengine.base.common import get_document, ALLOW_INHERITANCE from mongoengine.base.common import get_document, ALLOW_INHERITANCE
from mongoengine.base.datastructures import BaseDict, BaseList from mongoengine.base.datastructures import (
BaseDict,
BaseList,
EmbeddedDocumentList,
StrictDict,
SemiStrictDict
)
from mongoengine.base.fields import ComplexBaseField from mongoengine.base.fields import ComplexBaseField
__all__ = ('BaseDocument', 'NON_FIELD_ERRORS') __all__ = ('BaseDocument', 'NON_FIELD_ERRORS')
@@ -26,11 +31,12 @@ NON_FIELD_ERRORS = '__all__'
class BaseDocument(object): class BaseDocument(object):
__slots__ = ('_changed_fields', '_initialised', '_created', '_data',
'_dynamic_fields', '_auto_id_field', '_db_field_map', '__weakref__')
_dynamic = False _dynamic = False
_created = True
_dynamic_lock = True _dynamic_lock = True
_initialised = False STRICT = False
def __init__(self, *args, **values): def __init__(self, *args, **values):
""" """
@@ -39,6 +45,8 @@ class BaseDocument(object):
:param __auto_convert: Try and will cast python objects to Object types :param __auto_convert: Try and will cast python objects to Object types
:param values: A dictionary of values for the document :param values: A dictionary of values for the document
""" """
self._initialised = False
self._created = True
if args: if args:
# Combine positional arguments with named arguments. # Combine positional arguments with named arguments.
# We only want named arguments. # We only want named arguments.
@@ -49,21 +57,48 @@ class BaseDocument(object):
for value in args: for value in args:
name = next(field) name = next(field)
if name in values: if name in values:
raise TypeError("Multiple values for keyword argument '" + name + "'") raise TypeError(
"Multiple values for keyword argument '" + name + "'")
values[name] = value values[name] = value
__auto_convert = values.pop("__auto_convert", True) __auto_convert = values.pop("__auto_convert", True)
# 399: set default values only to fields loaded from DB
__only_fields = set(values.pop("__only_fields", values))
_created = values.pop("_created", True)
signals.pre_init.send(self.__class__, document=self, values=values) signals.pre_init.send(self.__class__, document=self, values=values)
# Check if there are undefined fields supplied, if so raise an
# Exception.
if not self._dynamic:
for var in values.keys():
if var not in self._fields.keys() + ['id', 'pk', '_cls', '_text_score']:
msg = (
"The field '{0}' does not exist on the document '{1}'"
).format(var, self._class_name)
raise FieldDoesNotExist(msg)
if self.STRICT and not self._dynamic:
self._data = StrictDict.create(allowed_keys=self._fields_ordered)()
else:
self._data = SemiStrictDict.create(
allowed_keys=self._fields_ordered)()
self._data = {} self._data = {}
self._dynamic_fields = SON() self._dynamic_fields = SON()
# Assign default values to instance # Assign default values to instance
for key, field in self._fields.iteritems(): for key, field in self._fields.iteritems():
if self._db_field_map.get(key, key) in values: if self._db_field_map.get(key, key) in __only_fields:
continue continue
value = getattr(self, key, None) value = getattr(self, key, None)
setattr(self, key, value) setattr(self, key, value)
if "_cls" not in values:
self._cls = self._class_name
# Set passed values after initialisation # Set passed values after initialisation
if self._dynamic: if self._dynamic:
dynamic_data = {} dynamic_data = {}
@@ -97,6 +132,7 @@ class BaseDocument(object):
# Flag initialised # Flag initialised
self._initialised = True self._initialised = True
self._created = _created
signals.post_init.send(self.__class__, document=self) signals.post_init.send(self.__class__, document=self)
def __delattr__(self, *args, **kwargs): def __delattr__(self, *args, **kwargs):
@@ -130,17 +166,25 @@ class BaseDocument(object):
self._data[name] = value self._data[name] = value
if hasattr(self, '_changed_fields'): if hasattr(self, '_changed_fields'):
self._mark_as_changed(name) self._mark_as_changed(name)
try:
self__created = self._created
except AttributeError:
self__created = True
if (self._is_document and not self._created and if (self._is_document and not self__created and
name in self._meta.get('shard_key', tuple()) and name in self._meta.get('shard_key', tuple()) and
self._data.get(name) != value): self._data.get(name) != value):
OperationError = _import_class('OperationError') OperationError = _import_class('OperationError')
msg = "Shard Keys are immutable. Tried to update %s" % name msg = "Shard Keys are immutable. Tried to update %s" % name
raise OperationError(msg) raise OperationError(msg)
try:
self__initialised = self._initialised
except AttributeError:
self__initialised = False
# Check if the user has created a new instance of a class # Check if the user has created a new instance of a class
if (self._is_document and self._initialised if (self._is_document and self__initialised
and self._created and name == self._meta['id_field']): and self__created and name == self._meta['id_field']):
super(BaseDocument, self).__setattr__('_created', False) super(BaseDocument, self).__setattr__('_created', False)
super(BaseDocument, self).__setattr__(name, value) super(BaseDocument, self).__setattr__(name, value)
@@ -158,9 +202,11 @@ class BaseDocument(object):
if isinstance(data["_data"], SON): if isinstance(data["_data"], SON):
data["_data"] = self.__class__._from_son(data["_data"])._data data["_data"] = self.__class__._from_son(data["_data"])._data
for k in ('_changed_fields', '_initialised', '_created', '_data', for k in ('_changed_fields', '_initialised', '_created', '_data',
'_fields_ordered', '_dynamic_fields'): '_dynamic_fields'):
if k in data: if k in data:
setattr(self, k, data[k]) setattr(self, k, data[k])
if '_fields_ordered' in data:
setattr(type(self), '_fields_ordered', data['_fields_ordered'])
dynamic_fields = data.get('_dynamic_fields') or SON() dynamic_fields = data.get('_dynamic_fields') or SON()
for k in dynamic_fields.keys(): for k in dynamic_fields.keys():
setattr(self, k, data["_data"].get(k)) setattr(self, k, data["_data"].get(k))
@@ -182,7 +228,7 @@ class BaseDocument(object):
"""Dictionary-style field access, set a field's value. """Dictionary-style field access, set a field's value.
""" """
# Ensure that the field exists before settings its value # Ensure that the field exists before settings its value
if name not in self._fields: if not self._dynamic and name not in self._fields:
raise KeyError(name) raise KeyError(name)
return setattr(self, name, value) return setattr(self, name, value)
@@ -201,7 +247,7 @@ class BaseDocument(object):
u = self.__str__() u = self.__str__()
except (UnicodeEncodeError, UnicodeDecodeError): except (UnicodeEncodeError, UnicodeDecodeError):
u = '[Bad Unicode data]' u = '[Bad Unicode data]'
repr_type = type(u) repr_type = str if u is None else type(u)
return repr_type('<%s: %s>' % (self.__class__.__name__, u)) return repr_type('<%s: %s>' % (self.__class__.__name__, u))
def __str__(self): def __str__(self):
@@ -213,9 +259,12 @@ class BaseDocument(object):
return txt_type('%s object' % self.__class__.__name__) return txt_type('%s object' % self.__class__.__name__)
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, self.__class__) and hasattr(other, 'id'): if isinstance(other, self.__class__) and hasattr(other, 'id') and other.id is not None:
if self.id == other.id: return self.id == other.id
return True if isinstance(other, DBRef):
return self._get_collection_name() == other.collection and self.id == other.id
if self.id is None:
return self is other
return False return False
def __ne__(self, other): def __ne__(self, other):
@@ -238,20 +287,55 @@ class BaseDocument(object):
""" """
pass pass
def to_mongo(self): def get_text_score(self):
"""Return as SON data ready for use with MongoDB.
""" """
Get text score from text query
"""
if '_text_score' not in self._data:
raise InvalidDocumentError('This document is not originally built from a text query')
return self._data['_text_score']
def to_mongo(self, use_db_field=True, fields=None):
"""
Return as SON data ready for use with MongoDB.
"""
if not fields:
fields = []
data = SON() data = SON()
data["_id"] = None data["_id"] = None
data['_cls'] = self._class_name data['_cls'] = self._class_name
EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
# only root fields ['test1.a', 'test2'] => ['test1', 'test2']
root_fields = set([f.split('.')[0] for f in fields])
for field_name in self: for field_name in self:
if root_fields and field_name not in root_fields:
continue
value = self._data.get(field_name, None) value = self._data.get(field_name, None)
field = self._fields.get(field_name) field = self._fields.get(field_name)
if field is None and self._dynamic: if field is None and self._dynamic:
field = self._dynamic_fields.get(field_name) field = self._dynamic_fields.get(field_name)
if value is not None: if value is not None:
if isinstance(field, (EmbeddedDocumentField)):
if fields:
key = '%s.' % field_name
embedded_fields = [
i.replace(key, '') for i in fields
if i.startswith(key)]
else:
embedded_fields = []
value = field.to_mongo(value, use_db_field=use_db_field,
fields=embedded_fields)
else:
value = field.to_mongo(value) value = field.to_mongo(value)
# Handle self generating fields # Handle self generating fields
@@ -260,7 +344,10 @@ class BaseDocument(object):
self._data[field_name] = value self._data[field_name] = value
if value is not None: if value is not None:
if use_db_field:
data[field.db_field] = value data[field.db_field] = value
else:
data[field.name] = value
# If "_id" has not been set, then try and set it # If "_id" has not been set, then try and set it
Document = _import_class("Document") Document = _import_class("Document")
@@ -295,7 +382,8 @@ class BaseDocument(object):
self._data.get(name)) for name in self._fields_ordered] self._data.get(name)) for name in self._fields_ordered]
EmbeddedDocumentField = _import_class("EmbeddedDocumentField") EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField") GenericEmbeddedDocumentField = _import_class(
"GenericEmbeddedDocumentField")
for field, value in fields: for field, value in fields:
if value is not None: if value is not None:
@@ -317,25 +405,30 @@ class BaseDocument(object):
pk = "None" pk = "None"
if hasattr(self, 'pk'): if hasattr(self, 'pk'):
pk = self.pk pk = self.pk
elif self._instance: elif self._instance and hasattr(self._instance, 'pk'):
pk = self._instance.pk pk = self._instance.pk
message = "ValidationError (%s:%s) " % (self._class_name, pk) message = "ValidationError (%s:%s) " % (self._class_name, pk)
raise ValidationError(message, errors=errors) raise ValidationError(message, errors=errors)
def to_json(self, *args, **kwargs): def to_json(self, *args, **kwargs):
"""Converts a document to JSON""" """Converts a document to JSON.
return json_util.dumps(self.to_mongo(), *args, **kwargs) :param use_db_field: Set to True by default but enables the output of the json structure with the field names and not the mongodb store db_names in case of set to False
"""
use_db_field = kwargs.pop('use_db_field', True)
return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs)
@classmethod @classmethod
def from_json(cls, json_data): def from_json(cls, json_data, created=False):
"""Converts json data to an unsaved document instance""" """Converts json data to an unsaved document instance"""
return cls._from_son(json_util.loads(json_data)) return cls._from_son(json_util.loads(json_data), created=created)
def __expand_dynamic_values(self, name, value): def __expand_dynamic_values(self, name, value):
"""expand any dynamic values to their correct types / values""" """expand any dynamic values to their correct types / values"""
if not isinstance(value, (dict, list, tuple)): if not isinstance(value, (dict, list, tuple)):
return value return value
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
is_list = False is_list = False
if not hasattr(value, 'items'): if not hasattr(value, 'items'):
is_list = True is_list = True
@@ -359,6 +452,9 @@ class BaseDocument(object):
# Convert lists / values so we can watch for any changes on them # Convert lists / values so we can watch for any changes on them
if (isinstance(value, (list, tuple)) and if (isinstance(value, (list, tuple)) and
not isinstance(value, BaseList)): not isinstance(value, BaseList)):
if issubclass(type(self), EmbeddedDocumentListField):
value = EmbeddedDocumentList(value, self, name)
else:
value = BaseList(value, self, name) value = BaseList(value, self, name)
elif isinstance(value, dict) and not isinstance(value, BaseDict): elif isinstance(value, dict) and not isinstance(value, BaseDict):
value = BaseDict(value, self, name) value = BaseDict(value, self, name)
@@ -370,9 +466,18 @@ class BaseDocument(object):
""" """
if not key: if not key:
return return
if not hasattr(self, '_changed_fields'):
return
if '.' in key:
key, rest = key.split('.', 1)
key = self._db_field_map.get(key, key) key = self._db_field_map.get(key, key)
if (hasattr(self, '_changed_fields') and key = '%s.%s' % (key, rest)
key not in self._changed_fields): else:
key = self._db_field_map.get(key, key)
if key not in self._changed_fields:
self._changed_fields.append(key) self._changed_fields.append(key)
def _clear_changed_fields(self): def _clear_changed_fields(self):
@@ -392,6 +497,8 @@ class BaseDocument(object):
else: else:
data = getattr(data, part, None) data = getattr(data, part, None)
if hasattr(data, "_changed_fields"): if hasattr(data, "_changed_fields"):
if hasattr(data, "_is_document") and data._is_document:
continue
data._changed_fields = [] data._changed_fields = []
self._changed_fields = [] self._changed_fields = []
@@ -405,12 +512,17 @@ class BaseDocument(object):
for index, value in iterator: for index, value in iterator:
list_key = "%s%s." % (key, index) list_key = "%s%s." % (key, index)
# don't check anything lower if this key is already marked
# as changed.
if list_key[:-1] in changed_fields:
continue
if hasattr(value, '_get_changed_fields'): if hasattr(value, '_get_changed_fields'):
changed = value._get_changed_fields(inspected) changed = value._get_changed_fields(inspected)
changed_fields += ["%s%s" % (list_key, k) changed_fields += ["%s%s" % (list_key, k)
for k in changed if k] for k in changed if k]
elif isinstance(value, (list, tuple, dict)): elif isinstance(value, (list, tuple, dict)):
self._nestable_types_changed_fields(changed_fields, list_key, value, inspected) self._nestable_types_changed_fields(
changed_fields, list_key, value, inspected)
def _get_changed_fields(self, inspected=None): def _get_changed_fields(self, inspected=None):
"""Returns a list of all fields that have explicitly been changed. """Returns a list of all fields that have explicitly been changed.
@@ -420,6 +532,7 @@ class BaseDocument(object):
ReferenceField = _import_class("ReferenceField") ReferenceField = _import_class("ReferenceField")
changed_fields = [] changed_fields = []
changed_fields += getattr(self, '_changed_fields', []) changed_fields += getattr(self, '_changed_fields', [])
inspected = inspected or set() inspected = inspected or set()
if hasattr(self, 'id') and isinstance(self.id, Hashable): if hasattr(self, 'id') and isinstance(self.id, Hashable):
if self.id in inspected: if self.id in inspected:
@@ -448,7 +561,8 @@ class BaseDocument(object):
if (hasattr(field, 'field') and if (hasattr(field, 'field') and
isinstance(field.field, ReferenceField)): isinstance(field.field, ReferenceField)):
continue continue
self._nestable_types_changed_fields(changed_fields, key, data, inspected) self._nestable_types_changed_fields(
changed_fields, key, data, inspected)
return changed_fields return changed_fields
def _delta(self): def _delta(self):
@@ -472,7 +586,10 @@ class BaseDocument(object):
if isinstance(d, (ObjectId, DBRef)): if isinstance(d, (ObjectId, DBRef)):
break break
elif isinstance(d, list) and p.isdigit(): elif isinstance(d, list) and p.isdigit():
try:
d = d[int(p)] d = d[int(p)]
except IndexError:
d = None
elif hasattr(d, 'get'): elif hasattr(d, 'get'):
d = d.get(p) d = d.get(p)
new_path.append(p) new_path.append(p)
@@ -537,18 +654,16 @@ class BaseDocument(object):
return cls._meta.get('collection', None) return cls._meta.get('collection', None)
@classmethod @classmethod
def _from_son(cls, son, _auto_dereference=True): def _from_son(cls, son, _auto_dereference=True, only_fields=None, created=False):
"""Create an instance of a Document (subclass) from a PyMongo SON. """Create an instance of a Document (subclass) from a PyMongo SON.
""" """
if not only_fields:
only_fields = []
# get the class name from the document, falling back to the given # get the class name from the document, falling back to the given
# class if unavailable # class if unavailable
class_name = son.get('_cls', cls._class_name) class_name = son.get('_cls', cls._class_name)
data = dict(("%s" % key, value) for key, value in son.iteritems()) data = dict(("%s" % key, value) for key, value in son.iteritems())
if not UNICODE_KWARGS:
# python 2.6.4 and lower cannot handle unicode keys
# passed to class constructor example: cls(**data)
to_str_keys_recursive(data)
# Return correct subclass for document type # Return correct subclass for document type
if class_name != cls._class_name: if class_name != cls._class_name:
@@ -578,6 +693,8 @@ class BaseDocument(object):
default = default() default = default()
if isinstance(default, BaseDocument): if isinstance(default, BaseDocument):
changed_fields.append(field_name) changed_fields.append(field_name)
elif not only_fields or field_name in only_fields:
changed_fields.append(field_name)
if errors_dict: if errors_dict:
errors = "\n".join(["%s - %s" % (k, v) errors = "\n".join(["%s - %s" % (k, v)
@@ -586,11 +703,14 @@ class BaseDocument(object):
% (cls._class_name, errors)) % (cls._class_name, errors))
raise InvalidDocumentError(msg) raise InvalidDocumentError(msg)
obj = cls(__auto_convert=False, **data) if cls.STRICT:
data = dict((k, v)
for k, v in data.iteritems() if k in cls._fields)
obj = cls(__auto_convert=False, _created=created, __only_fields=only_fields, **data)
obj._changed_fields = changed_fields obj._changed_fields = changed_fields
obj._created = False
if not _auto_dereference: if not _auto_dereference:
obj._fields = fields obj._fields = fields
return obj return obj
@classmethod @classmethod
@@ -609,7 +729,7 @@ class BaseDocument(object):
spec_fields = [v['fields'] spec_fields = [v['fields']
for k, v in enumerate(index_specs)] for k, v in enumerate(index_specs)]
# Merge unqiue_indexes with existing specs # Merge unique_indexes with existing specs
for k, v in enumerate(indices): for k, v in enumerate(indices):
if v['fields'] in spec_fields: if v['fields'] in spec_fields:
index_specs[spec_fields.index(v['fields'])].update(v) index_specs[spec_fields.index(v['fields'])].update(v)
@@ -640,6 +760,9 @@ class BaseDocument(object):
ALLOW_INHERITANCE) ALLOW_INHERITANCE)
include_cls = (allow_inheritance and not spec.get('sparse', False) and include_cls = (allow_inheritance and not spec.get('sparse', False) and
spec.get('cls', True)) spec.get('cls', True))
# 733: don't include cls if index_cls is False unless there is an explicit cls with the index
include_cls = include_cls and (spec.get('cls', False) or cls._meta.get('index_cls', True))
if "cls" in spec: if "cls" in spec:
spec.pop('cls') spec.pop('cls')
for key in spec['fields']: for key in spec['fields']:
@@ -647,15 +770,18 @@ class BaseDocument(object):
if isinstance(key, (list, tuple)): if isinstance(key, (list, tuple)):
continue continue
# ASCENDING from +, # ASCENDING from +
# DESCENDING from - # DESCENDING from -
# GEO2D from * # GEO2D from *
# TEXT from $
direction = pymongo.ASCENDING direction = pymongo.ASCENDING
if key.startswith("-"): if key.startswith("-"):
direction = pymongo.DESCENDING direction = pymongo.DESCENDING
elif key.startswith("*"): elif key.startswith("*"):
direction = pymongo.GEO2D direction = pymongo.GEO2D
if key.startswith(("+", "-", "*")): elif key.startswith("$"):
direction = pymongo.TEXT
if key.startswith(("+", "-", "*", "$")):
key = key[1:] key = key[1:]
# Use real field name, do it manually because we need field # Use real field name, do it manually because we need field
@@ -666,8 +792,14 @@ class BaseDocument(object):
fields = [] fields = []
else: else:
fields = cls._lookup_field(parts) fields = cls._lookup_field(parts)
parts = [field if field == '_id' else field.db_field parts = []
for field in fields] for field in fields:
try:
if field != "_id":
field = field.db_field
except AttributeError:
pass
parts.append(field)
key = '.'.join(parts) key = '.'.join(parts)
index_list.append((key, direction)) index_list.append((key, direction))
@@ -691,10 +823,9 @@ class BaseDocument(object):
""" """
unique_indexes = [] unique_indexes = []
for field_name, field in cls._fields.items(): for field_name, field in cls._fields.items():
sparse = False sparse = field.sparse
# Generate a list of indexes needed by uniqueness constraints # Generate a list of indexes needed by uniqueness constraints
if field.unique: if field.unique:
field.required = True
unique_fields = [field.db_field] unique_fields = [field.db_field]
# Add any unique_with fields to the back of the index spec # Add any unique_with fields to the back of the index spec
@@ -722,6 +853,9 @@ class BaseDocument(object):
index = {'fields': fields, 'unique': True, 'sparse': sparse} index = {'fields': fields, 'unique': True, 'sparse': sparse}
unique_indexes.append(index) unique_indexes.append(index)
if field.__class__.__name__ == "ListField":
field = field.field
# Grab any embedded document field unique indexes # Grab any embedded document field unique indexes
if (field.__class__.__name__ == "EmbeddedDocumentField" and if (field.__class__.__name__ == "EmbeddedDocumentField" and
field.document_type != cls): field.document_type != cls):
@@ -740,7 +874,8 @@ class BaseDocument(object):
geo_field_type_names = ["EmbeddedDocumentField", "GeoPointField", geo_field_type_names = ["EmbeddedDocumentField", "GeoPointField",
"PointField", "LineStringField", "PolygonField"] "PointField", "LineStringField", "PolygonField"]
geo_field_types = tuple([_import_class(field) for field in geo_field_type_names]) geo_field_types = tuple([_import_class(field)
for field in geo_field_type_names])
for field in cls._fields.values(): for field in cls._fields.values():
if not isinstance(field, geo_field_types): if not isinstance(field, geo_field_types):
@@ -750,7 +885,8 @@ class BaseDocument(object):
if field_cls in inspected: if field_cls in inspected:
continue continue
if hasattr(field_cls, '_geo_indices'): if hasattr(field_cls, '_geo_indices'):
geo_indices += field_cls._geo_indices(inspected, parent_field=field.db_field) geo_indices += field_cls._geo_indices(
inspected, parent_field=field.db_field)
elif field._geo_index: elif field._geo_index:
field_name = field.db_field field_name = field.db_field
if parent_field: if parent_field:
@@ -789,6 +925,19 @@ class BaseDocument(object):
elif cls._dynamic: elif cls._dynamic:
DynamicField = _import_class('DynamicField') DynamicField = _import_class('DynamicField')
field = DynamicField(db_field=field_name) field = DynamicField(db_field=field_name)
elif cls._meta.get("allow_inheritance", False) or cls._meta.get("abstract", False):
# 744: in case the field is defined in a subclass
field = None
for subcls in cls.__subclasses__():
try:
field = subcls._lookup_field([field_name])[0]
except LookUpError:
continue
if field is not None:
break
else:
raise LookUpError('Cannot resolve field "%s"' % field_name)
else: else:
raise LookUpError('Cannot resolve field "%s"' raise LookUpError('Cannot resolve field "%s"'
% field_name) % field_name)
@@ -804,8 +953,17 @@ class BaseDocument(object):
# Look up subfield on the previous field # Look up subfield on the previous field
new_field = field.lookup_member(field_name) new_field = field.lookup_member(field_name)
if not new_field and isinstance(field, ComplexBaseField): if not new_field and isinstance(field, ComplexBaseField):
if hasattr(field.field, 'document_type') and cls._dynamic \
and field.field.document_type._dynamic:
DynamicField = _import_class('DynamicField')
new_field = DynamicField(db_field=field_name)
else:
fields.append(field_name) fields.append(field_name)
continue continue
elif not new_field and hasattr(field, 'document_type') and cls._dynamic \
and field.document_type._dynamic:
DynamicField = _import_class('DynamicField')
new_field = DynamicField(db_field=field_name)
elif not new_field: elif not new_field:
raise LookUpError('Cannot resolve field "%s"' raise LookUpError('Cannot resolve field "%s"'
% field_name) % field_name)
@@ -825,7 +983,11 @@ class BaseDocument(object):
"""Dynamically set the display value for a field with choices""" """Dynamically set the display value for a field with choices"""
for attr_name, field in self._fields.items(): for attr_name, field in self._fields.items():
if field.choices: if field.choices:
setattr(self, if self._dynamic:
obj = self
else:
obj = type(self)
setattr(obj,
'get_%s_display' % attr_name, 'get_%s_display' % attr_name,
partial(self.__get_field_display, field=field)) partial(self.__get_field_display, field=field))

View File

@@ -9,12 +9,16 @@ from mongoengine.common import _import_class
from mongoengine.errors import ValidationError from mongoengine.errors import ValidationError
from mongoengine.base.common import ALLOW_INHERITANCE from mongoengine.base.common import ALLOW_INHERITANCE
from mongoengine.base.datastructures import BaseDict, BaseList from mongoengine.base.datastructures import (
BaseDict, BaseList, EmbeddedDocumentList
)
__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField") __all__ = ("BaseField", "ComplexBaseField",
"ObjectIdField", "GeoJsonBaseField")
class BaseField(object): class BaseField(object):
"""A base class for fields in a MongoDB document. Instances of this class """A base class for fields in a MongoDB document. Instances of this class
may be added to subclasses of `Document` to define a document's schema. may be added to subclasses of `Document` to define a document's schema.
@@ -35,7 +39,7 @@ class BaseField(object):
def __init__(self, db_field=None, name=None, required=False, default=None, def __init__(self, db_field=None, name=None, required=False, default=None,
unique=False, unique_with=None, primary_key=False, unique=False, unique_with=None, primary_key=False,
validation=None, choices=None, verbose_name=None, validation=None, choices=None, verbose_name=None,
help_text=None): help_text=None, null=False, sparse=False):
""" """
:param db_field: The database field to store this field in :param db_field: The database field to store this field in
(defaults to the name of the field) (defaults to the name of the field)
@@ -43,7 +47,7 @@ class BaseField(object):
:param required: If the field is required. Whether it has to have a :param required: If the field is required. Whether it has to have a
value or not. Defaults to False. value or not. Defaults to False.
:param default: (optional) The default value for this field if no value :param default: (optional) The default value for this field if no value
has been set (or if the value has been unset). It Can be a has been set (or if the value has been unset). It can be a
callable. callable.
:param unique: Is the field value unique or not. Defaults to False. :param unique: Is the field value unique or not. Defaults to False.
:param unique_with: (optional) The other field this field should be :param unique_with: (optional) The other field this field should be
@@ -58,8 +62,13 @@ class BaseField(object):
model forms from the document model. model forms from the document model.
:param help_text: (optional) The help text for this field and is often :param help_text: (optional) The help text for this field and is often
used when generating model forms from the document model. used when generating model forms from the document model.
:param null: (optional) Is the field value can be null. If no and there is a default value
then the default value is set
:param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False`
means that uniqueness won't be enforced for `None` values
""" """
self.db_field = (db_field or name) if not primary_key else '_id' self.db_field = (db_field or name) if not primary_key else '_id'
if name: if name:
msg = "Fields' 'name' attribute deprecated in favour of 'db_field'" msg = "Fields' 'name' attribute deprecated in favour of 'db_field'"
warnings.warn(msg, DeprecationWarning) warnings.warn(msg, DeprecationWarning)
@@ -72,6 +81,8 @@ class BaseField(object):
self.choices = choices self.choices = choices
self.verbose_name = verbose_name self.verbose_name = verbose_name
self.help_text = help_text self.help_text = help_text
self.null = null
self.sparse = sparse
# Adjust the appropriate creation counter, and save our local copy. # Adjust the appropriate creation counter, and save our local copy.
if self.db_field == '_id': if self.db_field == '_id':
@@ -97,7 +108,10 @@ class BaseField(object):
# If setting to None and theres a default # If setting to None and theres a default
# Then set the value to the default value # Then set the value to the default value
if value is None and self.default is not None: if value is None:
if self.null:
value = None
elif self.default is not None:
value = self.default value = self.default
if callable(value): if callable(value):
value = value() value = value()
@@ -113,7 +127,7 @@ class BaseField(object):
instance._mark_as_changed(self.name) instance._mark_as_changed(self.name)
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
if isinstance(value, EmbeddedDocument) and value._instance is None: if isinstance(value, EmbeddedDocument):
value._instance = weakref.proxy(instance) value._instance = weakref.proxy(instance)
instance._data[self.name] = value instance._data[self.name] = value
@@ -146,21 +160,23 @@ class BaseField(object):
def _validate(self, value, **kwargs): def _validate(self, value, **kwargs):
Document = _import_class('Document') Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
# check choices
# Check the Choices Constraint
if self.choices: if self.choices:
is_cls = isinstance(value, (Document, EmbeddedDocument))
value_to_check = value.__class__ if is_cls else value choice_list = self.choices
err_msg = 'an instance' if is_cls else 'one'
if isinstance(self.choices[0], (list, tuple)): if isinstance(self.choices[0], (list, tuple)):
option_keys = [k for k, v in self.choices] choice_list = [k for k, v in self.choices]
if value_to_check not in option_keys:
msg = ('Value must be %s of %s' % # Choices which are other types of Documents
(err_msg, unicode(option_keys))) if isinstance(value, (Document, EmbeddedDocument)):
self.error(msg) if not any(isinstance(value, c) for c in choice_list):
elif value_to_check not in self.choices: self.error(
msg = ('Value must be %s of %s' % 'Value must be instance of %s' % unicode(choice_list)
(err_msg, unicode(self.choices))) )
self.error(msg) # Choices which are types other than Documents
elif value not in choice_list:
self.error('Value must be one of %s' % unicode(choice_list))
# check validation argument # check validation argument
if self.validation is not None: if self.validation is not None:
@@ -175,6 +191,7 @@ class BaseField(object):
class ComplexBaseField(BaseField): class ComplexBaseField(BaseField):
"""Handles complex fields, such as lists / dictionaries. """Handles complex fields, such as lists / dictionaries.
Allows for nesting of embedded documents inside complex types. Allows for nesting of embedded documents inside complex types.
@@ -195,6 +212,7 @@ class ComplexBaseField(BaseField):
ReferenceField = _import_class('ReferenceField') ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class('GenericReferenceField') GenericReferenceField = _import_class('GenericReferenceField')
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
dereference = (self._auto_dereference and dereference = (self._auto_dereference and
(self.field is None or isinstance(self.field, (self.field is None or isinstance(self.field,
(GenericReferenceField, ReferenceField)))) (GenericReferenceField, ReferenceField))))
@@ -211,8 +229,11 @@ class ComplexBaseField(BaseField):
value = super(ComplexBaseField, self).__get__(instance, owner) value = super(ComplexBaseField, self).__get__(instance, owner)
# Convert lists / values so we can watch for any changes on them # Convert lists / values so we can watch for any changes on them
if (isinstance(value, (list, tuple)) and if isinstance(value, (list, tuple)):
not isinstance(value, BaseList)): if (issubclass(type(self), EmbeddedDocumentListField) and
not isinstance(value, EmbeddedDocumentList)):
value = EmbeddedDocumentList(value, instance, self.name)
elif not isinstance(value, BaseList):
value = BaseList(value, instance, self.name) value = BaseList(value, instance, self.name)
instance._data[self.name] = value instance._data[self.name] = value
elif isinstance(value, dict) and not isinstance(value, BaseDict): elif isinstance(value, dict) and not isinstance(value, BaseDict):
@@ -384,6 +405,7 @@ class ComplexBaseField(BaseField):
class ObjectIdField(BaseField): class ObjectIdField(BaseField):
"""A field wrapper around MongoDB's ObjectIds. """A field wrapper around MongoDB's ObjectIds.
""" """
@@ -412,7 +434,9 @@ class ObjectIdField(BaseField):
class GeoJsonBaseField(BaseField): class GeoJsonBaseField(BaseField):
"""A geo json field storing a geojson style object. """A geo json field storing a geojson style object.
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
@@ -421,8 +445,8 @@ class GeoJsonBaseField(BaseField):
def __init__(self, auto_index=True, *args, **kwargs): def __init__(self, auto_index=True, *args, **kwargs):
""" """
:param auto_index: Automatically create a "2dsphere" index. Defaults :param bool auto_index: Automatically create a "2dsphere" index.\
to `True`. Defaults to `True`.
""" """
self._name = "%sField" % self._type self._name = "%sField" % self._type
if not auto_index: if not auto_index:
@@ -435,7 +459,8 @@ class GeoJsonBaseField(BaseField):
if isinstance(value, dict): if isinstance(value, dict):
if set(value.keys()) == set(['type', 'coordinates']): if set(value.keys()) == set(['type', 'coordinates']):
if value['type'] != self._type: if value['type'] != self._type:
self.error('%s type must be "%s"' % (self._name, self._type)) self.error('%s type must be "%s"' %
(self._name, self._type))
return self.validate(value['coordinates']) return self.validate(value['coordinates'])
else: else:
self.error('%s can only accept a valid GeoJson dictionary' self.error('%s can only accept a valid GeoJson dictionary'
@@ -450,7 +475,7 @@ class GeoJsonBaseField(BaseField):
if error: if error:
self.error(error) self.error(error)
def _validate_polygon(self, value): def _validate_polygon(self, value, top_level=True):
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
return 'Polygons must contain list of linestrings' return 'Polygons must contain list of linestrings'
@@ -468,7 +493,10 @@ class GeoJsonBaseField(BaseField):
if error and error not in errors: if error and error not in errors:
errors.append(error) errors.append(error)
if errors: if errors:
if top_level:
return "Invalid Polygon:\n%s" % ", ".join(errors) return "Invalid Polygon:\n%s" % ", ".join(errors)
else:
return "%s" % ", ".join(errors)
def _validate_linestring(self, value, top_level=True): def _validate_linestring(self, value, top_level=True):
"""Validates a linestring""" """Validates a linestring"""
@@ -502,6 +530,66 @@ class GeoJsonBaseField(BaseField):
not isinstance(value[1], (float, int))): not isinstance(value[1], (float, int))):
return "Both values (%s) in point must be float or int" % repr(value) return "Both values (%s) in point must be float or int" % repr(value)
def _validate_multipoint(self, value):
if not isinstance(value, (list, tuple)):
return 'MultiPoint must be a list of Point'
# Quick and dirty validator
try:
value[0][0]
except:
return "Invalid MultiPoint must contain at least one valid point"
errors = []
for point in value:
error = self._validate_point(point)
if error and error not in errors:
errors.append(error)
if errors:
return "%s" % ", ".join(errors)
def _validate_multilinestring(self, value, top_level=True):
if not isinstance(value, (list, tuple)):
return 'MultiLineString must be a list of LineString'
# Quick and dirty validator
try:
value[0][0][0]
except:
return "Invalid MultiLineString must contain at least one valid linestring"
errors = []
for linestring in value:
error = self._validate_linestring(linestring, False)
if error and error not in errors:
errors.append(error)
if errors:
if top_level:
return "Invalid MultiLineString:\n%s" % ", ".join(errors)
else:
return "%s" % ", ".join(errors)
def _validate_multipolygon(self, value):
if not isinstance(value, (list, tuple)):
return 'MultiPolygon must be a list of Polygon'
# Quick and dirty validator
try:
value[0][0][0][0]
except:
return "Invalid MultiPolygon must contain at least one valid Polygon"
errors = []
for polygon in value:
error = self._validate_polygon(polygon, False)
if error and error not in errors:
errors.append(error)
if errors:
return "Invalid MultiPolygon:\n%s" % ", ".join(errors)
def to_mongo(self, value): def to_mongo(self, value):
if isinstance(value, dict): if isinstance(value, dict):
return value return value

View File

@@ -16,6 +16,7 @@ __all__ = ('DocumentMetaclass', 'TopLevelDocumentMetaclass')
class DocumentMetaclass(type): class DocumentMetaclass(type):
"""Metaclass for all documents. """Metaclass for all documents.
""" """
@@ -29,6 +30,7 @@ class DocumentMetaclass(type):
return super_new(cls, name, bases, attrs) return super_new(cls, name, bases, attrs)
attrs['_is_document'] = attrs.get('_is_document', False) attrs['_is_document'] = attrs.get('_is_document', False)
attrs['_cached_reference_fields'] = []
# EmbeddedDocuments could have meta data for inheritance # EmbeddedDocuments could have meta data for inheritance
if 'meta' in attrs: if 'meta' in attrs:
@@ -44,6 +46,11 @@ class DocumentMetaclass(type):
elif hasattr(base, '_meta'): elif hasattr(base, '_meta'):
meta.merge(base._meta) meta.merge(base._meta)
attrs['_meta'] = meta attrs['_meta'] = meta
attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract
if attrs['_meta'].get('allow_inheritance', ALLOW_INHERITANCE):
StringField = _import_class('StringField')
attrs['_cls'] = StringField()
# Handle document Fields # Handle document Fields
@@ -141,7 +148,8 @@ class DocumentMetaclass(type):
base._subclasses += (_cls,) base._subclasses += (_cls,)
base._types = base._subclasses # TODO depreciate _types base._types = base._subclasses # TODO depreciate _types
Document, EmbeddedDocument, DictField = cls._import_classes() (Document, EmbeddedDocument, DictField,
CachedReferenceField) = cls._import_classes()
if issubclass(new_class, Document): if issubclass(new_class, Document):
new_class._collection = None new_class._collection = None
@@ -170,6 +178,20 @@ class DocumentMetaclass(type):
f = field f = field
f.owner_document = new_class f.owner_document = new_class
delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING) delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING)
if isinstance(f, CachedReferenceField):
if issubclass(new_class, EmbeddedDocument):
raise InvalidDocumentError(
"CachedReferenceFields is not allowed in EmbeddedDocuments")
if not f.document_type:
raise InvalidDocumentError(
"Document is not avaiable to sync")
if f.auto_sync:
f.start_listener()
f.document_type._cached_reference_fields.append(f)
if isinstance(f, ComplexBaseField) and hasattr(f, 'field'): if isinstance(f, ComplexBaseField) and hasattr(f, 'field'):
delete_rule = getattr(f.field, delete_rule = getattr(f.field,
'reverse_delete_rule', 'reverse_delete_rule',
@@ -224,10 +246,12 @@ class DocumentMetaclass(type):
Document = _import_class('Document') Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
DictField = _import_class('DictField') DictField = _import_class('DictField')
return (Document, EmbeddedDocument, DictField) CachedReferenceField = _import_class('CachedReferenceField')
return (Document, EmbeddedDocument, DictField, CachedReferenceField)
class TopLevelDocumentMetaclass(DocumentMetaclass): class TopLevelDocumentMetaclass(DocumentMetaclass):
"""Metaclass for top-level documents (i.e. documents that have their own """Metaclass for top-level documents (i.e. documents that have their own
collection in the database. collection in the database.
""" """
@@ -359,7 +383,8 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
new_class.id = field new_class.id = field
# Set primary key if not defined by the document # Set primary key if not defined by the document
new_class._auto_id_field = False new_class._auto_id_field = getattr(parent_doc_cls,
'_auto_id_field', False)
if not new_class._meta.get('id_field'): if not new_class._meta.get('id_field'):
new_class._auto_id_field = True new_class._auto_id_field = True
new_class._meta['id_field'] = 'id' new_class._meta['id_field'] = 'id'
@@ -386,6 +411,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
class MetaDict(dict): class MetaDict(dict):
"""Custom dictionary for meta classes. """Custom dictionary for meta classes.
Handles the merging of set indexes Handles the merging of set indexes
""" """
@@ -400,5 +426,6 @@ class MetaDict(dict):
class BasesTuple(tuple): class BasesTuple(tuple):
"""Special class to handle introspection of bases tuple in __new__""" """Special class to handle introspection of bases tuple in __new__"""
pass pass

View File

@@ -1,4 +1,5 @@
_class_registry_cache = {} _class_registry_cache = {}
_field_list_cache = []
def _import_class(cls_name): def _import_class(cls_name):
@@ -20,12 +21,16 @@ def _import_class(cls_name):
doc_classes = ('Document', 'DynamicEmbeddedDocument', 'EmbeddedDocument', doc_classes = ('Document', 'DynamicEmbeddedDocument', 'EmbeddedDocument',
'MapReduceDocument') 'MapReduceDocument')
field_classes = ('DictField', 'DynamicField', 'EmbeddedDocumentField',
'FileField', 'GenericReferenceField', # Field Classes
'GenericEmbeddedDocumentField', 'GeoPointField', if not _field_list_cache:
'PointField', 'LineStringField', 'ListField', from mongoengine.fields import __all__ as fields
'PolygonField', 'ReferenceField', 'StringField', _field_list_cache.extend(fields)
'ComplexBaseField', 'GeoJsonBaseField') from mongoengine.base.fields import __all__ as fields
_field_list_cache.extend(fields)
field_classes = _field_list_cache
queryset_classes = ('OperationError',) queryset_classes = ('OperationError',)
deref_classes = ('DeReference',) deref_classes = ('DeReference',)

View File

@@ -18,9 +18,10 @@ _connections = {}
_dbs = {} _dbs = {}
def register_connection(alias, name, host=None, port=None, def register_connection(alias, name=None, host=None, port=None,
is_slave=False, read_preference=False, slaves=None, read_preference=False,
username=None, password=None, **kwargs): username=None, password=None, authentication_source=None,
**kwargs):
"""Add a connection. """Add a connection.
:param alias: the name that will be used to refer to this connection :param alias: the name that will be used to refer to this connection
@@ -28,28 +29,24 @@ def register_connection(alias, name, host=None, port=None,
:param name: the name of the specific database to use :param name: the name of the specific database to use
:param host: the host name of the :program:`mongod` instance to connect to :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 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 :param read_preference: The read preference for the collection
** Added pymongo 2.1 ** 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 username: username to authenticate with
:param password: password to authenticate with :param password: password to authenticate with
:param authentication_source: database to authenticate against
:param kwargs: allow ad-hoc parameters to be passed into the pymongo driver :param kwargs: allow ad-hoc parameters to be passed into the pymongo driver
""" """
global _connection_settings global _connection_settings
conn_settings = { conn_settings = {
'name': name, 'name': name or 'test',
'host': host or 'localhost', 'host': host or 'localhost',
'port': port or 27017, 'port': port or 27017,
'is_slave': is_slave, 'read_preference': read_preference,
'slaves': slaves or [],
'username': username, 'username': username,
'password': password, 'password': password,
'read_preference': read_preference 'authentication_source': authentication_source
} }
# Handle uri style connections # Handle uri style connections
@@ -64,6 +61,10 @@ def register_connection(alias, name, host=None, port=None,
if "replicaSet" in conn_settings['host']: if "replicaSet" in conn_settings['host']:
conn_settings['replicaSet'] = True conn_settings['replicaSet'] = True
# Deprecated parameters that should not be passed on
kwargs.pop('slaves', None)
kwargs.pop('is_slave', None)
conn_settings.update(kwargs) conn_settings.update(kwargs)
_connection_settings[alias] = conn_settings _connection_settings[alias] = conn_settings
@@ -93,20 +94,10 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
raise ConnectionError(msg) raise ConnectionError(msg)
conn_settings = _connection_settings[alias].copy() conn_settings = _connection_settings[alias].copy()
if hasattr(pymongo, 'version_tuple'): # Support for 2.1+
conn_settings.pop('name', None) conn_settings.pop('name', None)
conn_settings.pop('slaves', None)
conn_settings.pop('is_slave', None)
conn_settings.pop('username', None) conn_settings.pop('username', None)
conn_settings.pop('password', None) conn_settings.pop('password', None)
else: conn_settings.pop('authentication_source', None)
# 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 = MongoClient connection_class = MongoClient
if 'replicaSet' in conn_settings: if 'replicaSet' in conn_settings:
@@ -119,7 +110,18 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
connection_class = MongoReplicaSetClient connection_class = MongoReplicaSetClient
try: try:
_connections[alias] = connection_class(**conn_settings) connection = None
# check for shared connections
connection_settings_iterator = ((db_alias, settings.copy()) for db_alias, settings in _connection_settings.iteritems())
for db_alias, connection_settings in connection_settings_iterator:
connection_settings.pop('name', None)
connection_settings.pop('username', None)
connection_settings.pop('password', None)
if conn_settings == connection_settings and _connections.get(db_alias, None):
connection = _connections[db_alias]
break
_connections[alias] = connection if connection else connection_class(**conn_settings)
except Exception, e: except Exception, e:
raise ConnectionError("Cannot connect to database %s :\n%s" % (alias, e)) raise ConnectionError("Cannot connect to database %s :\n%s" % (alias, e))
return _connections[alias] return _connections[alias]
@@ -137,12 +139,13 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
# Authenticate if necessary # Authenticate if necessary
if conn_settings['username'] and conn_settings['password']: if conn_settings['username'] and conn_settings['password']:
db.authenticate(conn_settings['username'], db.authenticate(conn_settings['username'],
conn_settings['password']) conn_settings['password'],
source=conn_settings['authentication_source'])
_dbs[alias] = db _dbs[alias] = db
return _dbs[alias] return _dbs[alias]
def connect(db, alias=DEFAULT_CONNECTION_NAME, **kwargs): def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
"""Connect to the database specified by the 'db' argument. """Connect to the database specified by the 'db' argument.
Connection settings may be provided here as well if the database is not Connection settings may be provided here as well if the database is not

View File

@@ -1,6 +1,5 @@
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
from mongoengine.queryset import QuerySet
__all__ = ("switch_db", "switch_collection", "no_dereference", __all__ = ("switch_db", "switch_collection", "no_dereference",
@@ -162,12 +161,6 @@ class no_sub_classes(object):
return self.cls return self.cls
class QuerySetNoDeRef(QuerySet):
"""Special no_dereference QuerySet"""
def __dereference(items, max_depth=1, instance=None, name=None):
return items
class query_counter(object): class query_counter(object):
""" Query_counter context manager to get the number of queries. """ """ Query_counter context manager to get the number of queries. """

View File

@@ -1,6 +1,9 @@
from bson import DBRef, SON from bson import DBRef, SON
from base import (BaseDict, BaseList, TopLevelDocumentMetaclass, get_document) from base import (
BaseDict, BaseList, EmbeddedDocumentList,
TopLevelDocumentMetaclass, get_document
)
from fields import (ReferenceField, ListField, DictField, MapField) from fields import (ReferenceField, ListField, DictField, MapField)
from connection import get_db from connection import get_db
from queryset import QuerySet from queryset import QuerySet
@@ -12,7 +15,7 @@ class DeReference(object):
def __call__(self, items, max_depth=1, instance=None, name=None): def __call__(self, items, max_depth=1, instance=None, name=None):
""" """
Cheaply dereferences the items to a set depth. Cheaply dereferences the items to a set depth.
Also handles the convertion of complex data types. Also handles the conversion of complex data types.
:param items: The iterable (dict, list, queryset) to be dereferenced. :param items: The iterable (dict, list, queryset) to be dereferenced.
:param max_depth: The maximum depth to recurse to :param max_depth: The maximum depth to recurse to
@@ -36,7 +39,7 @@ class DeReference(object):
if instance and isinstance(instance, (Document, EmbeddedDocument, if instance and isinstance(instance, (Document, EmbeddedDocument,
TopLevelDocumentMetaclass)): TopLevelDocumentMetaclass)):
doc_type = instance._fields.get(name) doc_type = instance._fields.get(name)
if hasattr(doc_type, 'field'): while hasattr(doc_type, 'field'):
doc_type = doc_type.field doc_type = doc_type.field
if isinstance(doc_type, ReferenceField): if isinstance(doc_type, ReferenceField):
@@ -51,9 +54,19 @@ class DeReference(object):
return items return items
elif not field.dbref: elif not field.dbref:
if not hasattr(items, 'items'): if not hasattr(items, 'items'):
items = [field.to_python(v)
if not isinstance(v, (DBRef, Document)) else v def _get_items(items):
for v in items] new_items = []
for v in items:
if isinstance(v, list):
new_items.append(_get_items(v))
elif not isinstance(v, (DBRef, Document)):
new_items.append(field.to_python(v))
else:
new_items.append(v)
return new_items
items = _get_items(items)
else: else:
items = dict([ items = dict([
(k, field.to_python(v)) (k, field.to_python(v))
@@ -85,7 +98,7 @@ class DeReference(object):
# Recursively find dbreferences # Recursively find dbreferences
depth += 1 depth += 1
for k, item in iterator: for k, item in iterator:
if isinstance(item, Document): if isinstance(item, (Document, EmbeddedDocument)):
for field_name, field in item._fields.iteritems(): for field_name, field in item._fields.iteritems():
v = item._data.get(field_name, None) v = item._data.get(field_name, None)
if isinstance(v, (DBRef)): if isinstance(v, (DBRef)):
@@ -114,11 +127,11 @@ class DeReference(object):
"""Fetch all references and convert to their document objects """Fetch all references and convert to their document objects
""" """
object_map = {} object_map = {}
for col, dbrefs in self.reference_map.iteritems(): for collection, dbrefs in self.reference_map.iteritems():
keys = object_map.keys() keys = object_map.keys()
refs = list(set([dbref for dbref in dbrefs if unicode(dbref).encode('utf-8') not in keys])) refs = list(set([dbref for dbref in dbrefs if unicode(dbref).encode('utf-8') not in keys]))
if hasattr(col, 'objects'): # We have a document class for the refs if hasattr(collection, 'objects'): # We have a document class for the refs
references = col.objects.in_bulk(refs) references = collection.objects.in_bulk(refs)
for key, doc in references.iteritems(): for key, doc in references.iteritems():
object_map[key] = doc object_map[key] = doc
else: # Generic reference: use the refs data to convert to document else: # Generic reference: use the refs data to convert to document
@@ -126,19 +139,19 @@ class DeReference(object):
continue continue
if doc_type: if doc_type:
references = doc_type._get_db()[col].find({'_id': {'$in': refs}}) references = doc_type._get_db()[collection].find({'_id': {'$in': refs}})
for ref in references: for ref in references:
doc = doc_type._from_son(ref) doc = doc_type._from_son(ref)
object_map[doc.id] = doc object_map[doc.id] = doc
else: else:
references = get_db()[col].find({'_id': {'$in': refs}}) references = get_db()[collection].find({'_id': {'$in': refs}})
for ref in references: for ref in references:
if '_cls' in ref: if '_cls' in ref:
doc = get_document(ref["_cls"])._from_son(ref) doc = get_document(ref["_cls"])._from_son(ref)
elif doc_type is None: elif doc_type is None:
doc = get_document( doc = get_document(
''.join(x.capitalize() ''.join(x.capitalize()
for x in col.split('_')))._from_son(ref) for x in collection.split('_')))._from_son(ref)
else: else:
doc = doc_type._from_son(ref) doc = doc_type._from_son(ref)
object_map[doc.id] = doc object_map[doc.id] = doc
@@ -170,11 +183,18 @@ class DeReference(object):
return self.object_map.get(items['_ref'].id, items) return self.object_map.get(items['_ref'].id, items)
elif '_cls' in items: elif '_cls' in items:
doc = get_document(items['_cls'])._from_son(items) doc = get_document(items['_cls'])._from_son(items)
_cls = doc._data.pop('_cls', None)
del items['_cls']
doc._data = self._attach_objects(doc._data, depth, doc, None) doc._data = self._attach_objects(doc._data, depth, doc, None)
if _cls is not None:
doc._data['_cls'] = _cls
return doc return doc
if not hasattr(items, 'items'): if not hasattr(items, 'items'):
is_list = True is_list = True
list_type = BaseList
if isinstance(items, EmbeddedDocumentList):
list_type = EmbeddedDocumentList
as_tuple = isinstance(items, tuple) as_tuple = isinstance(items, tuple)
iterator = enumerate(items) iterator = enumerate(items)
data = [] data = []
@@ -192,7 +212,7 @@ class DeReference(object):
if k in self.object_map and not is_list: if k in self.object_map and not is_list:
data[k] = self.object_map[k] data[k] = self.object_map[k]
elif isinstance(v, Document): elif isinstance(v, (Document, EmbeddedDocument)):
for field_name, field in v._fields.iteritems(): for field_name, field in v._fields.iteritems():
v = data[k]._data.get(field_name, None) v = data[k]._data.get(field_name, None)
if isinstance(v, (DBRef)): if isinstance(v, (DBRef)):
@@ -204,13 +224,14 @@ class DeReference(object):
elif isinstance(v, (list, tuple)) and depth <= self.max_depth: elif isinstance(v, (list, tuple)) and depth <= self.max_depth:
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name) 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: elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
data[k] = self._attach_objects(v, depth - 1, instance=instance, name=name) item_name = '%s.%s' % (name, k) if name else name
data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name)
elif hasattr(v, 'id'): elif hasattr(v, 'id'):
data[k] = self.object_map.get(v.id, v) data[k] = self.object_map.get(v.id, v)
if instance and name: if instance and name:
if is_list: if is_list:
return tuple(data) if as_tuple else BaseList(data, instance, name) return tuple(data) if as_tuple else list_type(data, instance, name)
return BaseDict(data, instance, name) return BaseDict(data, instance, name)
depth += 1 depth += 1
return data return data

View File

@@ -3,7 +3,11 @@ from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import UserManager from django.contrib.auth.models import UserManager
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.utils.importlib import import_module try:
from django.utils.module_loading import import_module
except ImportError:
"""Handle older versions of Django"""
from django.utils.importlib import import_module
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@@ -1,39 +1,31 @@
#coding: utf-8 #coding: utf-8
from nose.plugins.skip import SkipTest
from mongoengine.python_support import PY3 from unittest import TestCase
from mongoengine import connect from mongoengine import connect
from mongoengine.connection import get_db
try:
from django.test import TestCase
from django.conf import settings
except Exception as err:
if PY3:
from unittest import TestCase
# Dummy value so no error
class settings:
MONGO_DATABASE_NAME = 'dummy'
else:
raise err
class MongoTestCase(TestCase): class MongoTestCase(TestCase):
def setUp(self):
if PY3:
raise SkipTest('django does not have Python 3 support')
""" """
TestCase class that clear the collection between the tests TestCase class that clear the collection between the tests
""" """
db_name = 'test_%s' % settings.MONGO_DATABASE_NAME
@property
def db_name(self):
from django.conf import settings
return 'test_%s' % getattr(settings, 'MONGO_DATABASE_NAME', 'dummy')
def __init__(self, methodName='runtest'): def __init__(self, methodName='runtest'):
self.db = connect(self.db_name).get_db() connect(self.db_name)
self.db = get_db()
super(MongoTestCase, self).__init__(methodName) super(MongoTestCase, self).__init__(methodName)
def _post_teardown(self): def dropCollections(self):
super(MongoTestCase, self)._post_teardown()
for collection in self.db.collection_names(): for collection in self.db.collection_names():
if collection == 'system.indexes': if collection.startswith('system.'):
continue continue
self.db.drop_collection(collection) self.db.drop_collection(collection)
def tearDown(self):
self.dropCollections()

View File

@@ -9,11 +9,19 @@ from bson import ObjectId
from bson.dbref import DBRef from bson.dbref import DBRef
from mongoengine import signals from mongoengine import signals
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.base import (DocumentMetaclass, TopLevelDocumentMetaclass, from mongoengine.base import (
BaseDocument, BaseDict, BaseList, DocumentMetaclass,
ALLOW_INHERITANCE, get_document) TopLevelDocumentMetaclass,
from mongoengine.errors import ValidationError BaseDocument,
from mongoengine.queryset import OperationError, NotUniqueError, QuerySet BaseDict,
BaseList,
EmbeddedDocumentList,
ALLOW_INHERITANCE,
get_document
)
from mongoengine.errors import ValidationError, InvalidQueryError, InvalidDocumentError
from mongoengine.queryset import (OperationError, NotUniqueError,
QuerySet, transform)
from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME
from mongoengine.context_managers import switch_db, switch_collection from mongoengine.context_managers import switch_db, switch_collection
@@ -40,6 +48,7 @@ class InvalidCollectionError(Exception):
class EmbeddedDocument(BaseDocument): class EmbeddedDocument(BaseDocument):
"""A :class:`~mongoengine.Document` that isn't stored in its own """A :class:`~mongoengine.Document` that isn't stored in its own
collection. :class:`~mongoengine.EmbeddedDocument`\ s should be used as collection. :class:`~mongoengine.EmbeddedDocument`\ s should be used as
fields on :class:`~mongoengine.Document`\ s through the fields on :class:`~mongoengine.Document`\ s through the
@@ -54,27 +63,35 @@ class EmbeddedDocument(BaseDocument):
dictionary. dictionary.
""" """
__slots__ = ('_instance')
# The __metaclass__ attribute is removed by 2to3 when running with Python3 # The __metaclass__ attribute is removed by 2to3 when running with Python3
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3 # my_metaclass is defined so that metaclass can be queried in Python 2 & 3
my_metaclass = DocumentMetaclass my_metaclass = DocumentMetaclass
__metaclass__ = DocumentMetaclass __metaclass__ = DocumentMetaclass
_instance = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EmbeddedDocument, self).__init__(*args, **kwargs) super(EmbeddedDocument, self).__init__(*args, **kwargs)
self._instance = None
self._changed_fields = [] self._changed_fields = []
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
return self.to_mongo() == other.to_mongo() return self._data == other._data
return False return False
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def save(self, *args, **kwargs):
self._instance.save(*args, **kwargs)
def reload(self, *args, **kwargs):
self._instance.reload(*args, **kwargs)
class Document(BaseDocument): class Document(BaseDocument):
"""The base class used for defining the structure and properties of """The base class used for defining the structure and properties of
collections of documents stored in MongoDB. Inherit from this class, and collections of documents stored in MongoDB. Inherit from this class, and
add fields as class attributes to define a document's structure. add fields as class attributes to define a document's structure.
@@ -109,7 +126,7 @@ class Document(BaseDocument):
a **+** or **-** sign. a **+** or **-** sign.
Automatic index creation can be disabled by specifying Automatic index creation can be disabled by specifying
attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to :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 False then indexes will not be created by MongoEngine. This is useful in
production systems where index creation is performed as part of a production systems where index creation is performed as part of a
deployment system. deployment system.
@@ -125,9 +142,12 @@ class Document(BaseDocument):
my_metaclass = TopLevelDocumentMetaclass my_metaclass = TopLevelDocumentMetaclass
__metaclass__ = TopLevelDocumentMetaclass __metaclass__ = TopLevelDocumentMetaclass
__slots__ = ('__objects')
def pk(): def pk():
"""Primary key alias """Primary key alias
""" """
def fget(self): def fget(self):
return getattr(self, self._meta['id_field']) return getattr(self, self._meta['id_field'])
@@ -178,9 +198,47 @@ class Document(BaseDocument):
cls.ensure_indexes() cls.ensure_indexes()
return cls._collection return cls._collection
def modify(self, query={}, **update):
"""Perform an atomic update of the document in the database and reload
the document object using updated version.
Returns True if the document has been updated or False if the document
in the database doesn't match the query.
.. note:: All unsaved changes that has been made to the document are
rejected if the method returns True.
:param query: the update will be performed only if the document in the
database matches the query
:param update: Django-style update keyword arguments
"""
if self.pk is None:
raise InvalidDocumentError("The document does not have a primary key.")
id_field = self._meta["id_field"]
query = query.copy() if isinstance(query, dict) else query.to_query(self)
if id_field not in query:
query[id_field] = self.pk
elif query[id_field] != self.pk:
raise InvalidQueryError("Invalid document modify query: it must modify only this document.")
updated = self._qs(**query).modify(new=True, **update)
if updated is None:
return False
for field in self._fields_ordered:
setattr(self, field, self._reload(field, updated[field]))
self._changed_fields = updated._changed_fields
self._created = False
return True
def save(self, force_insert=False, validate=True, clean=True, def save(self, force_insert=False, validate=True, clean=True,
write_concern=None, cascade=None, cascade_kwargs=None, write_concern=None, cascade=None, cascade_kwargs=None,
_refs=None, **kwargs): _refs=None, save_condition=None, **kwargs):
"""Save the :class:`~mongoengine.Document` to the database. If the """Save the :class:`~mongoengine.Document` to the database. If the
document already exists, it will be updated, otherwise it will be document already exists, it will be updated, otherwise it will be
created. created.
@@ -203,6 +261,8 @@ class Document(BaseDocument):
:param cascade_kwargs: (optional) kwargs dictionary to be passed throw :param cascade_kwargs: (optional) kwargs dictionary to be passed throw
to cascading saves. Implies ``cascade=True``. to cascading saves. Implies ``cascade=True``.
:param _refs: A list of processed references used in cascading saves :param _refs: A list of processed references used in cascading saves
:param save_condition: only perform save if matching record in db
satisfies condition(s) (e.g., version number)
.. versionchanged:: 0.5 .. versionchanged:: 0.5
In existing documents it only saves changed fields using In existing documents it only saves changed fields using
@@ -217,6 +277,9 @@ class Document(BaseDocument):
meta['cascade'] = True. Also you can pass different kwargs to meta['cascade'] = True. Also you can pass different kwargs to
the cascade save using cascade_kwargs which overwrites the the cascade save using cascade_kwargs which overwrites the
existing kwargs with custom values. existing kwargs with custom values.
.. versionchanged:: 0.8.5
Optional save_condition that only overwrites existing documents
if the condition is satisfied in the current db record.
""" """
signals.pre_save.send(self.__class__, document=self) signals.pre_save.send(self.__class__, document=self)
@@ -230,10 +293,13 @@ class Document(BaseDocument):
created = ('_id' not in doc or self._created or force_insert) created = ('_id' not in doc or self._created or force_insert)
signals.pre_save_post_validation.send(self.__class__, document=self, created=created) signals.pre_save_post_validation.send(self.__class__, document=self,
created=created)
try: try:
collection = self._get_collection() collection = self._get_collection()
if self._meta.get('auto_create_index', True):
self.ensure_indexes()
if created: if created:
if force_insert: if force_insert:
object_id = collection.insert(doc, **write_concern) object_id = collection.insert(doc, **write_concern)
@@ -243,7 +309,12 @@ class Document(BaseDocument):
object_id = doc['_id'] object_id = doc['_id']
updates, removals = self._delta() updates, removals = self._delta()
# Need to add shard key to query, or you get an error # Need to add shard key to query, or you get an error
select_dict = {'_id': object_id} if save_condition is not None:
select_dict = transform.query(self.__class__,
**save_condition)
else:
select_dict = {}
select_dict['_id'] = object_id
shard_key = self.__class__._meta.get('shard_key', tuple()) shard_key = self.__class__._meta.get('shard_key', tuple())
for k in shard_key: for k in shard_key:
actual_key = self._db_field_map.get(k, k) actual_key = self._db_field_map.get(k, k)
@@ -263,12 +334,14 @@ class Document(BaseDocument):
if removals: if removals:
update_query["$unset"] = removals update_query["$unset"] = removals
if updates or removals: if updates or removals:
upsert = save_condition is None
last_error = collection.update(select_dict, update_query, last_error = collection.update(select_dict, update_query,
upsert=True, **write_concern) upsert=upsert, **write_concern)
created = is_new_object(last_error) created = is_new_object(last_error)
if cascade is None: if cascade is None:
cascade = self._meta.get('cascade', False) or cascade_kwargs is not None cascade = self._meta.get(
'cascade', False) or cascade_kwargs is not None
if cascade: if cascade:
kwargs = { kwargs = {
@@ -293,12 +366,12 @@ class Document(BaseDocument):
raise NotUniqueError(message % unicode(err)) raise NotUniqueError(message % unicode(err))
raise OperationError(message % unicode(err)) raise OperationError(message % unicode(err))
id_field = self._meta['id_field'] id_field = self._meta['id_field']
if id_field not in self._meta.get('shard_key', []): if created or id_field not in self._meta.get('shard_key', []):
self[id_field] = self._fields[id_field].to_python(object_id) self[id_field] = self._fields[id_field].to_python(object_id)
signals.post_save.send(self.__class__, document=self, created=created)
self._clear_changed_fields() self._clear_changed_fields()
self._created = False self._created = False
signals.post_save.send(self.__class__, document=self, created=created)
return self return self
def cascade_save(self, *args, **kwargs): def cascade_save(self, *args, **kwargs):
@@ -361,7 +434,8 @@ class Document(BaseDocument):
del(query["_cls"]) del(query["_cls"])
return self._qs.filter(**query).update_one(**kwargs) return self._qs.filter(**query).update_one(**kwargs)
else: else:
raise OperationError('attempt to update a document not yet saved') raise OperationError(
'attempt to update a document not yet saved')
# Need to add shard key to query, or you get an error # Need to add shard key to query, or you get an error
return self._qs.filter(**self._object_key).update_one(**kwargs) return self._qs.filter(**self._object_key).update_one(**kwargs)
@@ -380,7 +454,8 @@ class Document(BaseDocument):
signals.pre_delete.send(self.__class__, document=self) signals.pre_delete.send(self.__class__, document=self)
try: try:
self._qs.filter(**self._object_key).delete(write_concern=write_concern, _from_doc_delete=True) self._qs.filter(
**self._object_key).delete(write_concern=write_concern, _from_doc_delete=True)
except pymongo.errors.OperationFailure, err: except pymongo.errors.OperationFailure, err:
message = u'Could not delete document (%s)' % err.message message = u'Could not delete document (%s)' % err.message
raise OperationError(message) raise OperationError(message)
@@ -396,10 +471,11 @@ class Document(BaseDocument):
user.switch_db('archive-db') user.switch_db('archive-db')
user.save() user.save()
If you need to read from another database see :param str db_alias: The database alias to use for saving the document
:class:`~mongoengine.context_managers.switch_db`
:param db_alias: The database alias to use for saving the document .. seealso::
Use :class:`~mongoengine.context_managers.switch_collection`
if you need to read from another collection
""" """
with switch_db(self.__class__, db_alias) as cls: with switch_db(self.__class__, db_alias) as cls:
collection = cls._get_collection() collection = cls._get_collection()
@@ -422,11 +498,12 @@ class Document(BaseDocument):
user.switch_collection('old-users') user.switch_collection('old-users')
user.save() user.save()
If you need to read from another database see :param str collection_name: The database alias to use for saving the
:class:`~mongoengine.context_managers.switch_db`
:param collection_name: The database alias to use for saving the
document document
.. seealso::
Use :class:`~mongoengine.context_managers.switch_db`
if you need to read from another database
""" """
with switch_collection(self.__class__, collection_name) as cls: with switch_collection(self.__class__, collection_name) as cls:
collection = cls._get_collection() collection = cls._get_collection()
@@ -447,27 +524,47 @@ class Document(BaseDocument):
DeReference()([self], max_depth + 1) DeReference()([self], max_depth + 1)
return self return self
def reload(self, max_depth=1): def reload(self, *fields, **kwargs):
"""Reloads all attributes from the database. """Reloads all attributes from the database.
:param fields: (optional) args list of fields to reload
:param max_depth: (optional) depth of dereferencing to follow
.. versionadded:: 0.1.2 .. versionadded:: 0.1.2
.. versionchanged:: 0.6 Now chainable .. versionchanged:: 0.6 Now chainable
.. versionchanged:: 0.9 Can provide specific fields to reload
""" """
max_depth = 1
if fields and isinstance(fields[0], int):
max_depth = fields[0]
fields = fields[1:]
elif "max_depth" in kwargs:
max_depth = kwargs["max_depth"]
if not self.pk: if not self.pk:
raise self.DoesNotExist("Document does not exist") raise self.DoesNotExist("Document does not exist")
obj = self._qs.read_preference(ReadPreference.PRIMARY).filter( obj = self._qs.read_preference(ReadPreference.PRIMARY).filter(
**self._object_key).limit(1).select_related(max_depth=max_depth) **self._object_key).only(*fields).limit(1
).select_related(max_depth=max_depth)
if obj: if obj:
obj = obj[0] obj = obj[0]
else: else:
raise self.DoesNotExist("Document does not exist") raise self.DoesNotExist("Document does not exist")
for field in self._fields_ordered: for field in self._fields_ordered:
if not fields or field in fields:
try:
setattr(self, field, self._reload(field, obj[field])) setattr(self, field, self._reload(field, obj[field]))
except KeyError:
# If field is removed from the database while the object
# is in memory, a reload would cause a KeyError
# i.e. obj.update(unset__field=1) followed by obj.reload()
delattr(self, field)
self._changed_fields = obj._changed_fields self._changed_fields = obj._changed_fields
self._created = False self._created = False
return obj return self
def _reload(self, key, value): def _reload(self, key, value):
"""Used by :meth:`~mongoengine.Document.reload` to ensure the """Used by :meth:`~mongoengine.Document.reload` to ensure the
@@ -476,6 +573,9 @@ class Document(BaseDocument):
if isinstance(value, BaseDict): if isinstance(value, BaseDict):
value = [(k, self._reload(k, v)) for k, v in value.items()] value = [(k, self._reload(k, v)) for k, v in value.items()]
value = BaseDict(value, self, key) value = BaseDict(value, self, key)
elif isinstance(value, EmbeddedDocumentList):
value = [self._reload(key, v) for v in value]
value = EmbeddedDocumentList(value, self, key)
elif isinstance(value, BaseList): elif isinstance(value, BaseList):
value = [self._reload(key, v) for v in value] value = [self._reload(key, v) for v in value]
value = BaseList(value, self, key) value = BaseList(value, self, key)
@@ -552,7 +652,9 @@ class Document(BaseDocument):
index_cls = cls._meta.get('index_cls', True) index_cls = cls._meta.get('index_cls', True)
collection = cls._get_collection() collection = cls._get_collection()
if collection.read_preference > 1: # 746: when connection is via mongos, the read preference is not necessarily an indication that
# this code runs on a secondary
if not collection.is_mongos and collection.read_preference > 1:
return return
# determine if an index which we are creating includes # determine if an index which we are creating includes
@@ -589,8 +691,9 @@ class Document(BaseDocument):
if cls._meta.get('abstract'): if cls._meta.get('abstract'):
return [] return []
# get all the base classes, subclasses and sieblings # get all the base classes, subclasses and siblings
classes = [] classes = []
def get_classes(cls): def get_classes(cls):
if (cls not in classes and if (cls not in classes and
@@ -648,7 +751,8 @@ class Document(BaseDocument):
""" """
required = cls.list_indexes() required = cls.list_indexes()
existing = [info['key'] for info in cls._get_collection().index_information().values()] existing = [info['key']
for info in cls._get_collection().index_information().values()]
missing = [index for index in required if index not in existing] missing = [index for index in required if index not in existing]
extra = [index for index in existing if index not in required] extra = [index for index in existing if index not in required]
@@ -666,6 +770,7 @@ class Document(BaseDocument):
class DynamicDocument(Document): class DynamicDocument(Document):
"""A Dynamic Document class allowing flexible, expandable and uncontrolled """A Dynamic Document class allowing flexible, expandable and uncontrolled
schemas. As a :class:`~mongoengine.Document` subclass, acts in the same schemas. As a :class:`~mongoengine.Document` subclass, acts in the same
way as an ordinary document but has expando style properties. Any data way as an ordinary document but has expando style properties. Any data
@@ -697,6 +802,7 @@ class DynamicDocument(Document):
class DynamicEmbeddedDocument(EmbeddedDocument): class DynamicEmbeddedDocument(EmbeddedDocument):
"""A Dynamic Embedded Document class allowing flexible, expandable and """A Dynamic Embedded Document class allowing flexible, expandable and
uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more
information about dynamic documents. information about dynamic documents.
@@ -723,6 +829,7 @@ class DynamicEmbeddedDocument(EmbeddedDocument):
class MapReduceDocument(object): class MapReduceDocument(object):
"""A document returned from a map/reduce query. """A document returned from a map/reduce query.
:param collection: An instance of :class:`~pymongo.Collection` :param collection: An instance of :class:`~pymongo.Collection`
@@ -753,7 +860,7 @@ class MapReduceDocument(object):
try: try:
self.key = id_field_type(self.key) self.key = id_field_type(self.key)
except: except:
raise Exception("Could not cast key as %s" % \ raise Exception("Could not cast key as %s" %
id_field_type.__name__) id_field_type.__name__)
if not hasattr(self, "_key_object"): if not hasattr(self, "_key_object"):

View File

@@ -5,7 +5,8 @@ from mongoengine.python_support import txt_type
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError', __all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError', 'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
'OperationError', 'NotUniqueError', 'ValidationError') 'OperationError', 'NotUniqueError', 'FieldDoesNotExist',
'ValidationError')
class NotRegistered(Exception): class NotRegistered(Exception):
@@ -40,6 +41,10 @@ class NotUniqueError(OperationError):
pass pass
class FieldDoesNotExist(Exception):
pass
class ValidationError(AssertionError): class ValidationError(AssertionError):
"""Validation exception. """Validation exception.

View File

@@ -34,22 +34,25 @@ except ImportError:
Image = None Image = None
ImageOps = None ImageOps = None
__all__ = ['StringField', 'URLField', 'EmailField', 'IntField', 'LongField', __all__ = [
'StringField', 'URLField', 'EmailField', 'IntField', 'LongField',
'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField', 'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField',
'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField', 'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField',
'GenericEmbeddedDocumentField', 'DynamicField', 'ListField', 'GenericEmbeddedDocumentField', 'DynamicField', 'ListField',
'SortedListField', 'DictField', 'MapField', 'ReferenceField', 'SortedListField', 'EmbeddedDocumentListField', 'DictField',
'GenericReferenceField', 'BinaryField', 'GridFSError', 'MapField', 'ReferenceField', 'CachedReferenceField',
'GridFSProxy', 'FileField', 'ImageGridFsProxy', 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy',
'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField',
'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField',
'GeoJsonBaseField'] 'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField',
'MultiPolygonField', 'GeoJsonBaseField']
RECURSIVE_REFERENCE_CONSTANT = 'self' RECURSIVE_REFERENCE_CONSTANT = 'self'
class StringField(BaseField): class StringField(BaseField):
"""A unicode string field. """A unicode string field.
""" """
@@ -109,6 +112,7 @@ class StringField(BaseField):
class URLField(StringField): class URLField(StringField):
"""A field that validates input as an URL. """A field that validates input as an URL.
.. versionadded:: 0.3 .. versionadded:: 0.3
@@ -116,7 +120,8 @@ class URLField(StringField):
_URL_REGEX = re.compile( _URL_REGEX = re.compile(
r'^(?:http|ftp)s?://' # http:// or https:// r'^(?:http|ftp)s?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... # domain...
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
r'localhost|' # localhost... r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port r'(?::\d+)?' # optional port
@@ -145,15 +150,19 @@ class URLField(StringField):
class EmailField(StringField): class EmailField(StringField):
"""A field that validates input as an E-Mail-Address. """A field that validates input as an E-Mail-Address.
.. versionadded:: 0.4 .. versionadded:: 0.4
""" """
EMAIL_REGEX = re.compile( EMAIL_REGEX = re.compile(
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom # dot-atom
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"
r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?\.)+[A-Z]{2,6}$', re.IGNORECASE # domain # quoted-string
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"'
# domain (max length of an ICAAN TLD is 22 characters)
r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?\.)+[A-Z]{2,22}$', re.IGNORECASE
) )
def validate(self, value): def validate(self, value):
@@ -163,6 +172,7 @@ class EmailField(StringField):
class IntField(BaseField): class IntField(BaseField):
"""An 32-bit integer field. """An 32-bit integer field.
""" """
@@ -197,6 +207,7 @@ class IntField(BaseField):
class LongField(BaseField): class LongField(BaseField):
"""An 64-bit integer field. """An 64-bit integer field.
""" """
@@ -231,6 +242,7 @@ class LongField(BaseField):
class FloatField(BaseField): class FloatField(BaseField):
"""An floating point number field. """An floating point number field.
""" """
@@ -265,6 +277,7 @@ class FloatField(BaseField):
class DecimalField(BaseField): class DecimalField(BaseField):
"""A fixed-point decimal number field. """A fixed-point decimal number field.
.. versionchanged:: 0.8 .. versionchanged:: 0.8
@@ -278,7 +291,7 @@ class DecimalField(BaseField):
:param max_value: Validation rule for the maximum acceptable value. :param max_value: Validation rule for the maximum acceptable value.
:param force_string: Store as a string. :param force_string: Store as a string.
:param precision: Number of decimal places to store. :param precision: Number of decimal places to store.
:param rounding: The rounding rule from the python decimal libary: :param rounding: The rounding rule from the python decimal library:
- decimal.ROUND_CEILING (towards Infinity) - decimal.ROUND_CEILING (towards Infinity)
- decimal.ROUND_DOWN (towards zero) - decimal.ROUND_DOWN (towards zero)
@@ -295,7 +308,7 @@ class DecimalField(BaseField):
self.min_value = min_value self.min_value = min_value
self.max_value = max_value self.max_value = max_value
self.force_string = force_string self.force_string = force_string
self.precision = decimal.Decimal(".%s" % ("0" * precision)) self.precision = precision
self.rounding = rounding self.rounding = rounding
super(DecimalField, self).__init__(**kwargs) super(DecimalField, self).__init__(**kwargs)
@@ -309,9 +322,9 @@ class DecimalField(BaseField):
value = decimal.Decimal("%s" % value) value = decimal.Decimal("%s" % value)
except decimal.InvalidOperation: except decimal.InvalidOperation:
return value return value
return value.quantize(self.precision, rounding=self.rounding) return value.quantize(decimal.Decimal(".%s" % ("0" * self.precision)), rounding=self.rounding)
def to_mongo(self, value): def to_mongo(self, value, use_db_field=True):
if value is None: if value is None:
return value return value
if self.force_string: if self.force_string:
@@ -338,6 +351,7 @@ class DecimalField(BaseField):
class BooleanField(BaseField): class BooleanField(BaseField):
"""A boolean field type. """A boolean field type.
.. versionadded:: 0.1.2 .. versionadded:: 0.1.2
@@ -356,15 +370,16 @@ class BooleanField(BaseField):
class DateTimeField(BaseField): class DateTimeField(BaseField):
"""A datetime field. """A datetime field.
Uses the python-dateutil library if available alternatively use time.strptime Uses the python-dateutil library if available alternatively use time.strptime
to parse the dates. Note: python-dateutil's parser is fully featured and when to parse the dates. Note: python-dateutil's parser is fully featured and when
installed you can utilise it to convert varing types of date formats into valid installed you can utilise it to convert varying types of date formats into valid
python datetime objects. python datetime objects.
Note: Microseconds are rounded to the nearest millisecond. Note: Microseconds are rounded to the nearest millisecond.
Pre UTC microsecond support is effecively broken. Pre UTC microsecond support is effectively broken.
Use :class:`~mongoengine.fields.ComplexDateTimeField` if you Use :class:`~mongoengine.fields.ComplexDateTimeField` if you
need accurate microsecond support. need accurate microsecond support.
""" """
@@ -391,7 +406,7 @@ class DateTimeField(BaseField):
if dateutil: if dateutil:
try: try:
return dateutil.parser.parse(value) return dateutil.parser.parse(value)
except ValueError: except (TypeError, ValueError):
return None return None
# split usecs, because they are not recognized by strptime. # split usecs, because they are not recognized by strptime.
@@ -423,6 +438,7 @@ class DateTimeField(BaseField):
class ComplexDateTimeField(StringField): class ComplexDateTimeField(StringField):
""" """
ComplexDateTimeField handles microseconds exactly instead of rounding ComplexDateTimeField handles microseconds exactly instead of rounding
like DateTimeField does. like DateTimeField does.
@@ -494,7 +510,7 @@ class ComplexDateTimeField(StringField):
def __get__(self, instance, owner): def __get__(self, instance, owner):
data = super(ComplexDateTimeField, self).__get__(instance, owner) data = super(ComplexDateTimeField, self).__get__(instance, owner)
if data is None: if data is None:
return datetime.datetime.now() return None if self.null else datetime.datetime.now()
if isinstance(data, datetime.datetime): if isinstance(data, datetime.datetime):
return data return data
return self._convert_from_string(data) return self._convert_from_string(data)
@@ -525,6 +541,7 @@ class ComplexDateTimeField(StringField):
class EmbeddedDocumentField(BaseField): class EmbeddedDocumentField(BaseField):
"""An embedded document field - with a declared document_type. """An embedded document field - with a declared document_type.
Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`. Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`.
""" """
@@ -551,10 +568,11 @@ class EmbeddedDocumentField(BaseField):
return self.document_type._from_son(value) return self.document_type._from_son(value)
return value return value
def to_mongo(self, value): def to_mongo(self, value, use_db_field=True, fields=[]):
if not isinstance(value, self.document_type): if not isinstance(value, self.document_type):
return value return value
return self.document_type.to_mongo(value) return self.document_type.to_mongo(value, use_db_field,
fields=fields)
def validate(self, value, clean=True): def validate(self, value, clean=True):
"""Make sure that the document instance is an instance of the """Make sure that the document instance is an instance of the
@@ -574,6 +592,7 @@ class EmbeddedDocumentField(BaseField):
class GenericEmbeddedDocumentField(BaseField): class GenericEmbeddedDocumentField(BaseField):
"""A generic embedded document field - allows any """A generic embedded document field - allows any
:class:`~mongoengine.EmbeddedDocument` to be stored. :class:`~mongoengine.EmbeddedDocument` to be stored.
@@ -601,24 +620,25 @@ class GenericEmbeddedDocumentField(BaseField):
value.validate(clean=clean) value.validate(clean=clean)
def to_mongo(self, document): def to_mongo(self, document, use_db_field=True):
if document is None: if document is None:
return None return None
data = document.to_mongo() data = document.to_mongo(use_db_field)
if not '_cls' in data: if not '_cls' in data:
data['_cls'] = document._class_name data['_cls'] = document._class_name
return data return data
class DynamicField(BaseField): class DynamicField(BaseField):
"""A truly dynamic field type capable of handling different and varying """A truly dynamic field type capable of handling different and varying
types of data. types of data.
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data""" Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
def to_mongo(self, value): def to_mongo(self, value):
"""Convert a Python type to a MongoDBcompatible type. """Convert a Python type to a MongoDB compatible type.
""" """
if isinstance(value, basestring): if isinstance(value, basestring):
@@ -675,6 +695,7 @@ class DynamicField(BaseField):
class ListField(ComplexBaseField): class ListField(ComplexBaseField):
"""A list field that wraps a standard field, allowing multiple instances """A list field that wraps a standard field, allowing multiple instances
of the field to be used as a list in the database. of the field to be used as a list in the database.
@@ -707,7 +728,34 @@ class ListField(ComplexBaseField):
return super(ListField, self).prepare_query_value(op, value) return super(ListField, self).prepare_query_value(op, value)
class EmbeddedDocumentListField(ListField):
"""A :class:`~mongoengine.ListField` designed specially to hold a list of
embedded documents to provide additional query helpers.
.. note::
The only valid list values are subclasses of
:class:`~mongoengine.EmbeddedDocument`.
.. versionadded:: 0.9
"""
def __init__(self, document_type, *args, **kwargs):
"""
:param document_type: The type of
:class:`~mongoengine.EmbeddedDocument` the list will hold.
:param args: Arguments passed directly into the parent
:class:`~mongoengine.ListField`.
:param kwargs: Keyword arguments passed directly into the parent
:class:`~mongoengine.ListField`.
"""
super(EmbeddedDocumentListField, self).__init__(
field=EmbeddedDocumentField(document_type), **kwargs
)
class SortedListField(ListField): class SortedListField(ListField):
"""A ListField that sorts the contents of its list before writing to """A ListField that sorts the contents of its list before writing to
the database in order to ensure that a sorted list is always the database in order to ensure that a sorted list is always
retrieved. retrieved.
@@ -739,6 +787,7 @@ class SortedListField(ListField):
reverse=self._order_reverse) reverse=self._order_reverse)
return sorted(value, reverse=self._order_reverse) return sorted(value, reverse=self._order_reverse)
def key_not_string(d): def key_not_string(d):
""" Helper function to recursively determine if any key in a dictionary is """ Helper function to recursively determine if any key in a dictionary is
not a string. not a string.
@@ -747,6 +796,7 @@ def key_not_string(d):
if not isinstance(k, basestring) or (isinstance(v, dict) and key_not_string(v)): if not isinstance(k, basestring) or (isinstance(v, dict) and key_not_string(v)):
return True return True
def key_has_dot_or_dollar(d): def key_has_dot_or_dollar(d):
""" Helper function to recursively determine if any key in a dictionary """ Helper function to recursively determine if any key in a dictionary
contains a dot or a dollar sign. contains a dot or a dollar sign.
@@ -755,12 +805,14 @@ def key_has_dot_or_dollar(d):
if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)): if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)):
return True return True
class DictField(ComplexBaseField): class DictField(ComplexBaseField):
"""A dictionary field that wraps a standard Python dictionary. This is """A dictionary field that wraps a standard Python dictionary. This is
similar to an embedded document, but the structure is not defined. similar to an embedded document, but the structure is not defined.
.. note:: .. note::
Required means it cannot be empty - as the default for ListFields is [] Required means it cannot be empty - as the default for DictFields is {}
.. versionadded:: 0.3 .. versionadded:: 0.3
.. versionchanged:: 0.5 - Can now handle complex / varying types of data .. versionchanged:: 0.5 - Can now handle complex / varying types of data
@@ -801,12 +853,17 @@ class DictField(ComplexBaseField):
return StringField().prepare_query_value(op, value) return StringField().prepare_query_value(op, value)
if hasattr(self.field, 'field'): if hasattr(self.field, 'field'):
if op in ('set', 'unset') and isinstance(value, dict):
return dict(
(k, self.field.prepare_query_value(op, v))
for k, v in value.items())
return self.field.prepare_query_value(op, value) return self.field.prepare_query_value(op, value)
return super(DictField, self).prepare_query_value(op, value) return super(DictField, self).prepare_query_value(op, value)
class MapField(DictField): class MapField(DictField):
"""A field that maps a name to a specified field type. Similar to """A field that maps a name to a specified field type. Similar to
a DictField, except the 'value' of each item must match the specified a DictField, except the 'value' of each item must match the specified
field type. field type.
@@ -822,12 +879,13 @@ class MapField(DictField):
class ReferenceField(BaseField): class ReferenceField(BaseField):
"""A reference to a document that will be automatically dereferenced on """A reference to a document that will be automatically dereferenced on
access (lazily). access (lazily).
Use the `reverse_delete_rule` to handle what should happen if the document Use the `reverse_delete_rule` to handle what should happen if the document
the field is referencing is deleted. EmbeddedDocuments, DictFields and the field is referencing is deleted. EmbeddedDocuments, DictFields and
MapFields do not support reverse_delete_rules and an `InvalidDocumentError` MapFields does not support reverse_delete_rule and an `InvalidDocumentError`
will be raised if trying to set on one of these Document / Field types. will be raised if trying to set on one of these Document / Field types.
The options are: The options are:
@@ -851,7 +909,7 @@ class ReferenceField(BaseField):
Bar.register_delete_rule(Foo, 'bar', NULLIFY) Bar.register_delete_rule(Foo, 'bar', NULLIFY)
.. note :: .. note ::
`reverse_delete_rules` do not trigger pre / post delete signals to be `reverse_delete_rule` does not trigger pre / post delete signals to be
triggered. triggered.
.. versionchanged:: 0.5 added `reverse_delete_rule` .. versionchanged:: 0.5 added `reverse_delete_rule`
@@ -955,7 +1013,147 @@ class ReferenceField(BaseField):
return self.document_type._fields.get(member_name) return self.document_type._fields.get(member_name)
class CachedReferenceField(BaseField):
"""
A referencefield with cache fields to porpuse pseudo-joins
.. versionadded:: 0.9
"""
def __init__(self, document_type, fields=[], auto_sync=True, **kwargs):
"""Initialises the Cached Reference Field.
:param fields: A list of fields to be cached in document
:param auto_sync: if True documents are auto updated.
"""
if not isinstance(document_type, basestring) and \
not issubclass(document_type, (Document, basestring)):
self.error('Argument to CachedReferenceField constructor must be a'
' document class or a string')
self.auto_sync = auto_sync
self.document_type_obj = document_type
self.fields = fields
super(CachedReferenceField, self).__init__(**kwargs)
def start_listener(self):
from mongoengine import signals
signals.post_save.connect(self.on_document_pre_save,
sender=self.document_type)
def on_document_pre_save(self, sender, document, created, **kwargs):
if not created:
update_kwargs = dict(
('set__%s__%s' % (self.name, k), v)
for k, v in document._delta()[0].items()
if k in self.fields)
if update_kwargs:
filter_kwargs = {}
filter_kwargs[self.name] = document
self.owner_document.objects(
**filter_kwargs).update(**update_kwargs)
def to_python(self, value):
if isinstance(value, dict):
collection = self.document_type._get_collection_name()
value = DBRef(
collection, self.document_type.id.to_python(value['_id']))
return value
@property
def document_type(self):
if isinstance(self.document_type_obj, basestring):
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
self.document_type_obj = self.owner_document
else:
self.document_type_obj = get_document(self.document_type_obj)
return self.document_type_obj
def __get__(self, instance, owner):
if instance is None:
# Document class being used rather than a document object
return self
# Get value from document instance if available
value = instance._data.get(self.name)
self._auto_dereference = instance._fields[self.name]._auto_dereference
# Dereference DBRefs
if self._auto_dereference and 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(CachedReferenceField, self).__get__(instance, owner)
def to_mongo(self, document):
id_field_name = self.document_type._meta['id_field']
id_field = self.document_type._fields[id_field_name]
doc_tipe = self.document_type
if isinstance(document, Document):
# We need the id from the saved object to create the DBRef
id_ = document.pk
if id_ is None:
self.error('You can only reference documents once they have'
' been saved to the database')
else:
self.error('Only accept a document object')
value = SON((
("_id", id_field.to_mongo(id_)),
))
value.update(dict(document.to_mongo(fields=self.fields)))
return value
def prepare_query_value(self, op, value):
if value is None:
return None
if isinstance(value, Document):
if value.pk is None:
self.error('You can only reference documents once they have'
' been saved to the database')
return {'_id': value.pk}
raise NotImplementedError
def validate(self, value):
if not isinstance(value, (self.document_type)):
self.error("A CachedReferenceField only accepts documents")
if isinstance(value, Document) and value.id is None:
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)
def sync_all(self):
"""
Sync all cached fields on demand.
Caution: this operation may be slower.
"""
update_key = 'set__%s' % self.name
for doc in self.document_type.objects:
filter_kwargs = {}
filter_kwargs[self.name] = doc
update_kwargs = {}
update_kwargs[update_key] = doc
self.owner_document.objects(
**filter_kwargs).update(**update_kwargs)
class GenericReferenceField(BaseField): class GenericReferenceField(BaseField):
"""A reference to *any* :class:`~mongoengine.document.Document` subclass """A reference to *any* :class:`~mongoengine.document.Document` subclass
that will be automatically dereferenced on access (lazily). that will be automatically dereferenced on access (lazily).
@@ -974,6 +1172,7 @@ class GenericReferenceField(BaseField):
return self return self
value = instance._data.get(self.name) value = instance._data.get(self.name)
self._auto_dereference = instance._fields[self.name]._auto_dereference self._auto_dereference = instance._fields[self.name]._auto_dereference
if self._auto_dereference and isinstance(value, (dict, SON)): if self._auto_dereference and isinstance(value, (dict, SON)):
instance._data[self.name] = self.dereference(value) instance._data[self.name] = self.dereference(value)
@@ -1001,7 +1200,7 @@ class GenericReferenceField(BaseField):
doc = doc_cls._from_son(doc) doc = doc_cls._from_son(doc)
return doc return doc
def to_mongo(self, document): def to_mongo(self, document, use_db_field=True):
if document is None: if document is None:
return None return None
@@ -1036,6 +1235,7 @@ class GenericReferenceField(BaseField):
class BinaryField(BaseField): class BinaryField(BaseField):
"""A binary data field. """A binary data field.
""" """
@@ -1067,6 +1267,7 @@ class GridFSError(Exception):
class GridFSProxy(object): class GridFSProxy(object):
"""Proxy object to handle writing and reading of files to and from GridFS """Proxy object to handle writing and reading of files to and from GridFS
.. versionadded:: 0.4 .. versionadded:: 0.4
@@ -1121,7 +1322,8 @@ class GridFSProxy(object):
return '<%s: %s>' % (self.__class__.__name__, self.grid_id) return '<%s: %s>' % (self.__class__.__name__, self.grid_id)
def __str__(self): def __str__(self):
name = getattr(self.get(), 'filename', self.grid_id) if self.get() else '(no file)' name = getattr(
self.get(), 'filename', self.grid_id) if self.get() else '(no file)'
return '<%s: %s>' % (self.__class__.__name__, name) return '<%s: %s>' % (self.__class__.__name__, name)
def __eq__(self, other): def __eq__(self, other):
@@ -1135,7 +1337,8 @@ class GridFSProxy(object):
@property @property
def fs(self): def fs(self):
if not self._fs: if not self._fs:
self._fs = gridfs.GridFS(get_db(self.db_alias), self.collection_name) self._fs = gridfs.GridFS(
get_db(self.db_alias), self.collection_name)
return self._fs return self._fs
def get(self, id=None): def get(self, id=None):
@@ -1154,6 +1357,7 @@ class GridFSProxy(object):
def new_file(self, **kwargs): def new_file(self, **kwargs):
self.newfile = self.fs.new_file(**kwargs) self.newfile = self.fs.new_file(**kwargs)
self.grid_id = self.newfile._id self.grid_id = self.newfile._id
self._mark_as_changed()
def put(self, file_obj, **kwargs): def put(self, file_obj, **kwargs):
if self.grid_id: if self.grid_id:
@@ -1209,6 +1413,7 @@ class GridFSProxy(object):
class FileField(BaseField): class FileField(BaseField):
"""A GridFS storage field. """A GridFS storage field.
.. versionadded:: 0.4 .. versionadded:: 0.4
@@ -1253,7 +1458,8 @@ class FileField(BaseField):
pass pass
# Create a new proxy object as we don't already have one # Create a new proxy object as we don't already have one
instance._data[key] = self.get_proxy_obj(key=key, instance=instance) instance._data[key] = self.get_proxy_obj(
key=key, instance=instance)
instance._data[key].put(value) instance._data[key].put(value)
else: else:
instance._data[key] = value instance._data[key] = value
@@ -1291,11 +1497,13 @@ class FileField(BaseField):
class ImageGridFsProxy(GridFSProxy): class ImageGridFsProxy(GridFSProxy):
""" """
Proxy for ImageField Proxy for ImageField
versionadded: 0.6 versionadded: 0.6
""" """
def put(self, file_obj, **kwargs): def put(self, file_obj, **kwargs):
""" """
Insert a image in database Insert a image in database
@@ -1312,6 +1520,16 @@ class ImageGridFsProxy(GridFSProxy):
except Exception, e: except Exception, e:
raise ValidationError('Invalid image: %s' % e) raise ValidationError('Invalid image: %s' % e)
# Progressive JPEG
progressive = img.info.get('progressive') or False
if (kwargs.get('progressive') and
isinstance(kwargs.get('progressive'), bool) and
img_format == 'JPEG'):
progressive = True
else:
progressive = False
if (field.size and (img.size[0] > field.size['width'] or if (field.size and (img.size[0] > field.size['width'] or
img.size[1] > field.size['height'])): img.size[1] > field.size['height'])):
size = field.size size = field.size
@@ -1331,7 +1549,8 @@ class ImageGridFsProxy(GridFSProxy):
size = field.thumbnail_size size = field.thumbnail_size
if size['force']: if size['force']:
thumbnail = ImageOps.fit(img, (size['width'], size['height']), Image.ANTIALIAS) thumbnail = ImageOps.fit(
img, (size['width'], size['height']), Image.ANTIALIAS)
else: else:
thumbnail = img.copy() thumbnail = img.copy()
thumbnail.thumbnail((size['width'], thumbnail.thumbnail((size['width'],
@@ -1339,14 +1558,14 @@ class ImageGridFsProxy(GridFSProxy):
Image.ANTIALIAS) Image.ANTIALIAS)
if thumbnail: if thumbnail:
thumb_id = self._put_thumbnail(thumbnail, img_format) thumb_id = self._put_thumbnail(thumbnail, img_format, progressive)
else: else:
thumb_id = None thumb_id = None
w, h = img.size w, h = img.size
io = StringIO() io = StringIO()
img.save(io, img_format) img.save(io, img_format, progressive=progressive)
io.seek(0) io.seek(0)
return super(ImageGridFsProxy, self).put(io, return super(ImageGridFsProxy, self).put(io,
@@ -1357,18 +1576,18 @@ class ImageGridFsProxy(GridFSProxy):
**kwargs) **kwargs)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
#deletes thumbnail # deletes thumbnail
out = self.get() out = self.get()
if out and out.thumbnail_id: if out and out.thumbnail_id:
self.fs.delete(out.thumbnail_id) self.fs.delete(out.thumbnail_id)
return super(ImageGridFsProxy, self).delete(*args, **kwargs) return super(ImageGridFsProxy, self).delete(*args, **kwargs)
def _put_thumbnail(self, thumbnail, format, **kwargs): def _put_thumbnail(self, thumbnail, format, progressive, **kwargs):
w, h = thumbnail.size w, h = thumbnail.size
io = StringIO() io = StringIO()
thumbnail.save(io, format) thumbnail.save(io, format, progressive=progressive)
io.seek(0) io.seek(0)
return self.fs.put(io, width=w, return self.fs.put(io, width=w,
@@ -1417,6 +1636,7 @@ class ImproperlyConfigured(Exception):
class ImageField(FileField): class ImageField(FileField):
""" """
A Image File storage field. A Image File storage field.
@@ -1455,7 +1675,8 @@ class ImageField(FileField):
class SequenceField(BaseField): class SequenceField(BaseField):
"""Provides a sequental counter see:
"""Provides a sequential counter see:
http://www.mongodb.org/display/DOCS/Object+IDs#ObjectIDs-SequenceNumbers http://www.mongodb.org/display/DOCS/Object+IDs#ObjectIDs-SequenceNumbers
.. note:: .. note::
@@ -1524,7 +1745,7 @@ class SequenceField(BaseField):
data = collection.find_one({"_id": sequence_id}) data = collection.find_one({"_id": sequence_id})
if data: if data:
return self.value_decorator(data['next']+1) return self.value_decorator(data['next'] + 1)
return self.value_decorator(1) return self.value_decorator(1)
@@ -1554,6 +1775,14 @@ class SequenceField(BaseField):
return super(SequenceField, self).__set__(instance, value) return super(SequenceField, self).__set__(instance, value)
def prepare_query_value(self, op, value):
"""
This method is overridden in order to convert the query value into to required
type. We need to do this in order to be able to successfully compare query
values passed as string, the base implementation returns the value as is.
"""
return self.value_decorator(value)
def to_python(self, value): def to_python(self, value):
if value is None: if value is None:
value = self.generate() value = self.generate()
@@ -1561,6 +1790,7 @@ class SequenceField(BaseField):
class UUIDField(BaseField): class UUIDField(BaseField):
"""A UUID field. """A UUID field.
.. versionadded:: 0.6 .. versionadded:: 0.6
@@ -1613,7 +1843,13 @@ class UUIDField(BaseField):
class GeoPointField(BaseField): class GeoPointField(BaseField):
"""A list storing a latitude and longitude.
"""A list storing a longitude and latitude coordinate.
.. note:: this represents a generic point in a 2D plane and a legacy way of
representing a geo point. It admits 2d indexes but not "2dsphere" indexes
in MongoDB > 2.4 which are more natural for modeling geospatial points.
See :ref:`geospatial-indexes`
.. versionadded:: 0.4 .. versionadded:: 0.4
""" """
@@ -1628,14 +1864,17 @@ class GeoPointField(BaseField):
'of (x, y)') 'of (x, y)')
if not len(value) == 2: if not len(value) == 2:
self.error("Value (%s) must be a two-dimensional point" % repr(value)) self.error("Value (%s) must be a two-dimensional point" %
repr(value))
elif (not isinstance(value[0], (float, int)) or elif (not isinstance(value[0], (float, int)) or
not isinstance(value[1], (float, int))): not isinstance(value[1], (float, int))):
self.error("Both values (%s) in point must be float or int" % repr(value)) self.error(
"Both values (%s) in point must be float or int" % repr(value))
class PointField(GeoJsonBaseField): class PointField(GeoJsonBaseField):
"""A geo json field storing a latitude and longitude.
"""A GeoJSON field storing a longitude and latitude coordinate.
The data is represented as: The data is represented as:
@@ -1648,13 +1887,15 @@ class PointField(GeoJsonBaseField):
to set the value. to set the value.
Requires mongodb >= 2.4 Requires mongodb >= 2.4
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
_type = "Point" _type = "Point"
class LineStringField(GeoJsonBaseField): class LineStringField(GeoJsonBaseField):
"""A geo json field storing a line of latitude and longitude coordinates.
"""A GeoJSON field storing a line of longitude and latitude coordinates.
The data is represented as: The data is represented as:
@@ -1666,13 +1907,15 @@ class LineStringField(GeoJsonBaseField):
You can either pass a dict with the full information or a list of points. You can either pass a dict with the full information or a list of points.
Requires mongodb >= 2.4 Requires mongodb >= 2.4
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
_type = "LineString" _type = "LineString"
class PolygonField(GeoJsonBaseField): class PolygonField(GeoJsonBaseField):
"""A geo json field storing a polygon of latitude and longitude coordinates.
"""A GeoJSON field storing a polygon of longitude and latitude coordinates.
The data is represented as: The data is represented as:
@@ -1687,6 +1930,77 @@ class PolygonField(GeoJsonBaseField):
holes. holes.
Requires mongodb >= 2.4 Requires mongodb >= 2.4
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
_type = "Polygon" _type = "Polygon"
class MultiPointField(GeoJsonBaseField):
"""A GeoJSON field storing a list of Points.
The data is represented as:
.. code-block:: js
{ "type" : "MultiPoint" ,
"coordinates" : [[x1, y1], [x2, y2]]}
You can either pass a dict with the full information or a list
to set the value.
Requires mongodb >= 2.6
.. versionadded:: 0.9
"""
_type = "MultiPoint"
class MultiLineStringField(GeoJsonBaseField):
"""A GeoJSON field storing a list of LineStrings.
The data is represented as:
.. code-block:: js
{ "type" : "MultiLineString" ,
"coordinates" : [[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]]}
You can either pass a dict with the full information or a list of points.
Requires mongodb >= 2.6
.. versionadded:: 0.9
"""
_type = "MultiLineString"
class MultiPolygonField(GeoJsonBaseField):
"""A GeoJSON field storing list of Polygons.
The data is represented as:
.. code-block:: js
{ "type" : "MultiPolygon" ,
"coordinates" : [[
[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]
], [
[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]
]
}
You can either pass a dict with the full information or a list
of Polygons.
Requires mongodb >= 2.6
.. versionadded:: 0.9
"""
_type = "MultiPolygon"

View File

@@ -3,8 +3,6 @@
import sys import sys
PY3 = sys.version_info[0] == 3 PY3 = sys.version_info[0] == 3
PY25 = sys.version_info[:2] == (2, 5)
UNICODE_KWARGS = int(''.join([str(x) for x in sys.version_info[:3]])) > 264
if PY3: if PY3:
import codecs import codecs
@@ -29,33 +27,3 @@ else:
txt_type = unicode txt_type = unicode
str_types = (bin_type, txt_type) str_types = (bin_type, txt_type)
if PY25:
def product(*args, **kwds):
pools = map(tuple, args) * kwds.get('repeat', 1)
result = [[]]
for pool in pools:
result = [x + [y] for x in result for y in pool]
for prod in result:
yield tuple(prod)
reduce = reduce
else:
from itertools import product
from functools import reduce
# For use with Python 2.5
# converts all keys from unicode to str for d and all nested dictionaries
def to_str_keys_recursive(d):
if isinstance(d, list):
for val in d:
if isinstance(val, (dict, list)):
to_str_keys_recursive(val)
elif isinstance(d, dict):
for key, val in d.items():
if isinstance(val, (dict, list)):
to_str_keys_recursive(val)
if isinstance(key, unicode):
d[str(key)] = d.pop(key)
else:
raise ValueError("non list/dict parameter not allowed")

View File

@@ -7,17 +7,20 @@ import pprint
import re import re
import warnings import warnings
from bson import SON
from bson.code import Code from bson.code import Code
from bson import json_util from bson import json_util
import pymongo import pymongo
import pymongo.errors
from pymongo.common import validate_read_preference from pymongo.common import validate_read_preference
from mongoengine import signals from mongoengine import signals
from mongoengine.connection import get_db
from mongoengine.context_managers import switch_db
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.base.common import get_document from mongoengine.base.common import get_document
from mongoengine.errors import (OperationError, NotUniqueError, from mongoengine.errors import (OperationError, NotUniqueError,
InvalidQueryError, LookUpError) InvalidQueryError, LookUpError)
from mongoengine.queryset import transform from mongoengine.queryset import transform
from mongoengine.queryset.field_list import QueryFieldList from mongoengine.queryset.field_list import QueryFieldList
from mongoengine.queryset.visitor import Q, QNode from mongoengine.queryset.visitor import Q, QNode
@@ -36,6 +39,7 @@ RE_TYPE = type(re.compile(''))
class BaseQuerySet(object): class BaseQuerySet(object):
"""A set of results returned from a query. Wraps a MongoDB cursor, """A set of results returned from a query. Wraps a MongoDB cursor,
providing :class:`~mongoengine.Document` objects as the results. providing :class:`~mongoengine.Document` objects as the results.
""" """
@@ -50,7 +54,7 @@ class BaseQuerySet(object):
self._initial_query = {} self._initial_query = {}
self._where_clause = None self._where_clause = None
self._loaded_fields = QueryFieldList() self._loaded_fields = QueryFieldList()
self._ordering = [] self._ordering = None
self._snapshot = False self._snapshot = False
self._timeout = True self._timeout = True
self._class_check = True self._class_check = True
@@ -61,6 +65,7 @@ class BaseQuerySet(object):
self._none = False self._none = False
self._as_pymongo = False self._as_pymongo = False
self._as_pymongo_coerce = False self._as_pymongo_coerce = False
self._search_text = None
# If inheritance is allowed, only return instances and instances of # If inheritance is allowed, only return instances and instances of
# subclasses of the class being used # subclasses of the class being used
@@ -68,12 +73,15 @@ class BaseQuerySet(object):
if len(self._document._subclasses) == 1: if len(self._document._subclasses) == 1:
self._initial_query = {"_cls": self._document._subclasses[0]} self._initial_query = {"_cls": self._document._subclasses[0]}
else: else:
self._initial_query = {"_cls": {"$in": self._document._subclasses}} self._initial_query = {
"_cls": {"$in": self._document._subclasses}}
self._loaded_fields = QueryFieldList(always_include=['_cls']) self._loaded_fields = QueryFieldList(always_include=['_cls'])
self._cursor_obj = None self._cursor_obj = None
self._limit = None self._limit = None
self._skip = None self._skip = None
self._hint = -1 # Using -1 as None is a valid value for hint self._hint = -1 # Using -1 as None is a valid value for hint
self.only_fields = []
self._max_time_ms = None
def __call__(self, q_obj=None, class_check=True, slave_okay=False, def __call__(self, q_obj=None, class_check=True, slave_okay=False,
read_preference=None, **query): read_preference=None, **query):
@@ -144,16 +152,35 @@ class BaseQuerySet(object):
if queryset._scalar: if queryset._scalar:
return queryset._get_scalar( return queryset._get_scalar(
queryset._document._from_son(queryset._cursor[key], queryset._document._from_son(queryset._cursor[key],
_auto_dereference=self._auto_dereference)) _auto_dereference=self._auto_dereference,
only_fields=self.only_fields))
if queryset._as_pymongo: if queryset._as_pymongo:
return queryset._get_as_pymongo(queryset._cursor.next()) return queryset._get_as_pymongo(queryset._cursor[key])
return queryset._document._from_son(queryset._cursor[key], return queryset._document._from_son(queryset._cursor[key],
_auto_dereference=self._auto_dereference) _auto_dereference=self._auto_dereference, only_fields=self.only_fields)
raise AttributeError raise AttributeError
def __iter__(self): def __iter__(self):
raise NotImplementedError raise NotImplementedError
def _has_data(self):
""" Retrieves whether cursor has any data. """
queryset = self.order_by()
return False if queryset.first() is None else True
def __nonzero__(self):
""" Avoid to open all records in an if stmt in Py2. """
return self._has_data()
def __bool__(self):
""" Avoid to open all records in an if stmt in Py3. """
return self._has_data()
# Core functions # Core functions
def all(self): def all(self):
@@ -165,6 +192,32 @@ class BaseQuerySet(object):
""" """
return self.__call__(*q_objs, **query) return self.__call__(*q_objs, **query)
def search_text(self, text, language=None):
"""
Start a text search, using text indexes.
Require: MongoDB server version 2.6+.
:param language: The language that determines the list of stop words
for the search and the rules for the stemmer and tokenizer.
If not specified, the search uses the default language of the index.
For supported languages, see `Text Search Languages <http://docs.mongodb.org/manual/reference/text-search-languages/#text-search-languages>`.
"""
queryset = self.clone()
if queryset._search_text:
raise OperationError(
"It is not possible to use search_text two times.")
query_kwargs = SON({'$search': text})
if language:
query_kwargs['$language'] = language
queryset._query_obj &= Q(__raw__={'$text': query_kwargs})
queryset._mongo_query = None
queryset._cursor_obj = None
queryset._search_text = text
return queryset
def get(self, *q_objs, **query): def get(self, *q_objs, **query):
"""Retrieve the the matching object raising """Retrieve the the matching object raising
:class:`~mongoengine.queryset.MultipleObjectsReturned` or :class:`~mongoengine.queryset.MultipleObjectsReturned` or
@@ -175,7 +228,7 @@ class BaseQuerySet(object):
.. versionadded:: 0.3 .. versionadded:: 0.3
""" """
queryset = self.clone() queryset = self.clone()
queryset = queryset.limit(2) queryset = queryset.order_by().limit(2)
queryset = queryset.filter(*q_objs, **query) queryset = queryset.filter(*q_objs, **query)
try: try:
@@ -215,7 +268,7 @@ class BaseQuerySet(object):
.. note:: This requires two separate operations and therefore a .. note:: This requires two separate operations and therefore a
race condition exists. Because there are no transactions in race condition exists. Because there are no transactions in
mongoDB other approaches should be investigated, to ensure you mongoDB other approaches should be investigated, to ensure you
don't accidently duplicate data when using this method. This is don't accidentally duplicate data when using this method. This is
now scheduled to be removed before 1.0 now scheduled to be removed before 1.0
:param write_concern: optional extra keyword arguments used if we :param write_concern: optional extra keyword arguments used if we
@@ -303,10 +356,10 @@ class BaseQuerySet(object):
try: try:
ids = self._collection.insert(raw, **write_concern) ids = self._collection.insert(raw, **write_concern)
except pymongo.errors.DuplicateKeyError, err: except pymongo.errors.DuplicateKeyError, err:
message = 'Could not save document (%s)'; message = 'Could not save document (%s)'
raise NotUniqueError(message % unicode(err)) raise NotUniqueError(message % unicode(err))
except pymongo.errors.OperationFailure, err: except pymongo.errors.OperationFailure, err:
message = 'Could not save document (%s)'; message = 'Could not save document (%s)'
if re.match('^E1100[01] duplicate key', unicode(err)): if re.match('^E1100[01] duplicate key', unicode(err)):
# E11000 - duplicate key error index # E11000 - duplicate key error index
# E11001 - duplicate key on update # E11001 - duplicate key on update
@@ -327,7 +380,7 @@ class BaseQuerySet(object):
self._document, documents=results, loaded=True) self._document, documents=results, loaded=True)
return return_one and results[0] or results return return_one and results[0] or results
def count(self, with_limit_and_skip=True): def count(self, with_limit_and_skip=False):
"""Count the selected elements in the query. """Count the selected elements in the query.
:param with_limit_and_skip (optional): take any :meth:`limit` or :param with_limit_and_skip (optional): take any :meth:`limit` or
@@ -349,6 +402,8 @@ class BaseQuerySet(object):
will force an fsync on the primary server. will force an fsync on the primary server.
:param _from_doc_delete: True when called from document delete therefore :param _from_doc_delete: True when called from document delete therefore
signals will have been triggered so don't loop. signals will have been triggered so don't loop.
:returns number of deleted documents
""" """
queryset = self.clone() queryset = self.clone()
doc = queryset._document doc = queryset._document
@@ -366,15 +421,19 @@ class BaseQuerySet(object):
has_delete_signal) and not _from_doc_delete has_delete_signal) and not _from_doc_delete
if call_document_delete: if call_document_delete:
cnt = 0
for doc in queryset: for doc in queryset:
doc.delete(write_concern=write_concern) doc.delete(write_concern=write_concern)
return cnt += 1
return cnt
delete_rules = doc._meta.get('delete_rules') or {} delete_rules = doc._meta.get('delete_rules') or {}
# Check for DENY rules before actually deleting/nullifying any other # Check for DENY rules before actually deleting/nullifying any other
# references # references
for rule_entry in delete_rules: for rule_entry in delete_rules:
document_cls, field_name = rule_entry document_cls, field_name = rule_entry
if document_cls._meta.get('abstract'):
continue
rule = doc._meta['delete_rules'][rule_entry] rule = doc._meta['delete_rules'][rule_entry]
if rule == DENY and document_cls.objects( if rule == DENY and document_cls.objects(
**{field_name + '__in': self}).count() > 0: **{field_name + '__in': self}).count() > 0:
@@ -384,6 +443,8 @@ class BaseQuerySet(object):
for rule_entry in delete_rules: for rule_entry in delete_rules:
document_cls, field_name = rule_entry document_cls, field_name = rule_entry
if document_cls._meta.get('abstract'):
continue
rule = doc._meta['delete_rules'][rule_entry] rule = doc._meta['delete_rules'][rule_entry]
if rule == CASCADE: if rule == CASCADE:
ref_q = document_cls.objects(**{field_name + '__in': self}) ref_q = document_cls.objects(**{field_name + '__in': self})
@@ -399,7 +460,8 @@ class BaseQuerySet(object):
write_concern=write_concern, write_concern=write_concern,
**{'pull_all__%s' % field_name: self}) **{'pull_all__%s' % field_name: self})
queryset._collection.remove(queryset._query, write_concern=write_concern) result = queryset._collection.remove(queryset._query, **write_concern)
return result["n"]
def update(self, upsert=False, multi=True, write_concern=None, def update(self, upsert=False, multi=True, write_concern=None,
full_result=False, **update): full_result=False, **update):
@@ -443,6 +505,8 @@ class BaseQuerySet(object):
return result return result
elif result: elif result:
return result['n'] return result['n']
except pymongo.errors.DuplicateKeyError, err:
raise NotUniqueError(u'Update failed (%s)' % unicode(err))
except pymongo.errors.OperationFailure, err: except pymongo.errors.OperationFailure, err:
if unicode(err) == u'multi not coded yet': if unicode(err) == u'multi not coded yet':
message = u'update() method requires MongoDB 1.1.3+' message = u'update() method requires MongoDB 1.1.3+'
@@ -466,6 +530,60 @@ class BaseQuerySet(object):
return self.update( return self.update(
upsert=upsert, multi=False, write_concern=write_concern, **update) upsert=upsert, multi=False, write_concern=write_concern, **update)
def modify(self, upsert=False, full_response=False, remove=False, new=False, **update):
"""Update and return the updated document.
Returns either the document before or after modification based on `new`
parameter. If no documents match the query and `upsert` is false,
returns ``None``. If upserting and `new` is false, returns ``None``.
If the full_response parameter is ``True``, the return value will be
the entire response object from the server, including the 'ok' and
'lastErrorObject' fields, rather than just the modified document.
This is useful mainly because the 'lastErrorObject' document holds
information about the command's execution.
:param upsert: insert if document doesn't exist (default ``False``)
:param full_response: return the entire response object from the
server (default ``False``)
:param remove: remove rather than updating (default ``False``)
:param new: return updated rather than original document
(default ``False``)
:param update: Django-style update keyword arguments
.. versionadded:: 0.9
"""
if remove and new:
raise OperationError("Conflicting parameters: remove and new")
if not update and not upsert and not remove:
raise OperationError(
"No update parameters, must either update or remove")
queryset = self.clone()
query = queryset._query
update = transform.update(queryset._document, **update)
sort = queryset._ordering
try:
result = queryset._collection.find_and_modify(
query, update, upsert=upsert, sort=sort, remove=remove, new=new,
full_response=full_response, **self._cursor_args)
except pymongo.errors.DuplicateKeyError, err:
raise NotUniqueError(u"Update failed (%s)" % err)
except pymongo.errors.OperationFailure, err:
raise OperationError(u"Update failed (%s)" % err)
if full_response:
if result["value"] is not None:
result["value"] = self._document._from_son(result["value"], only_fields=self.only_fields)
else:
if result is not None:
result = self._document._from_son(result, only_fields=self.only_fields)
return result
def with_id(self, object_id): def with_id(self, object_id):
"""Retrieve the object matching the id provided. Uses `object_id` only """Retrieve the object matching the id provided. Uses `object_id` only
and raises InvalidQueryError if a filter has been applied. Returns and raises InvalidQueryError if a filter has been applied. Returns
@@ -497,13 +615,15 @@ class BaseQuerySet(object):
if self._scalar: if self._scalar:
for doc in docs: for doc in docs:
doc_map[doc['_id']] = self._get_scalar( doc_map[doc['_id']] = self._get_scalar(
self._document._from_son(doc)) self._document._from_son(doc, only_fields=self.only_fields))
elif self._as_pymongo: elif self._as_pymongo:
for doc in docs: for doc in docs:
doc_map[doc['_id']] = self._get_as_pymongo(doc) doc_map[doc['_id']] = self._get_as_pymongo(doc)
else: else:
for doc in docs: for doc in docs:
doc_map[doc['_id']] = self._document._from_son(doc) doc_map[doc['_id']] = self._document._from_son(doc,
only_fields=self.only_fields,
_auto_dereference=self._auto_dereference)
return doc_map return doc_map
@@ -522,6 +642,19 @@ class BaseQuerySet(object):
return self return self
def using(self, alias):
"""This method is for controlling which database the QuerySet will be evaluated against if you are using more than one database.
:param alias: The database alias
.. versionadded:: 0.9
"""
with switch_db(self._document, alias) as cls:
collection = cls._get_collection()
return self.clone_into(self.__class__(self._document, collection))
def clone(self): def clone(self):
"""Creates a copy of the current """Creates a copy of the current
:class:`~mongoengine.queryset.QuerySet` :class:`~mongoengine.queryset.QuerySet`
@@ -535,13 +668,15 @@ class BaseQuerySet(object):
:class:`~mongoengine.queryset.base.BaseQuerySet` into another child class :class:`~mongoengine.queryset.base.BaseQuerySet` into another child class
""" """
if not isinstance(cls, BaseQuerySet): if not isinstance(cls, BaseQuerySet):
raise OperationError('%s is not a subclass of BaseQuerySet' % cls.__name__) raise OperationError(
'%s is not a subclass of BaseQuerySet' % cls.__name__)
copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj', copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj',
'_where_clause', '_loaded_fields', '_ordering', '_snapshot', '_where_clause', '_loaded_fields', '_ordering', '_snapshot',
'_timeout', '_class_check', '_slave_okay', '_read_preference', '_timeout', '_class_check', '_slave_okay', '_read_preference',
'_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce',
'_limit', '_skip', '_hint', '_auto_dereference') '_limit', '_skip', '_hint', '_auto_dereference',
'_search_text', 'only_fields', '_max_time_ms')
for prop in copy_props: for prop in copy_props:
val = getattr(self, prop) val = getattr(self, prop)
@@ -627,10 +762,31 @@ class BaseQuerySet(object):
distinct = self._dereference(queryset._cursor.distinct(field), 1, distinct = self._dereference(queryset._cursor.distinct(field), 1,
name=field, instance=self._document) name=field, instance=self._document)
doc_field = self._document._fields.get(field.split('.', 1)[0])
instance = False
# We may need to cast to the correct type eg. ListField(EmbeddedDocumentField) # We may need to cast to the correct type eg. ListField(EmbeddedDocumentField)
doc_field = getattr(self._document._fields.get(field), "field", None) EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
ListField = _import_class('ListField')
GenericEmbeddedDocumentField = _import_class('GenericEmbeddedDocumentField')
if isinstance(doc_field, ListField):
doc_field = getattr(doc_field, "field", doc_field)
if isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)):
instance = getattr(doc_field, "document_type", False) instance = getattr(doc_field, "document_type", False)
if instance: # handle distinct on subdocuments
if '.' in field:
for field_part in field.split('.')[1:]:
# if looping on embedded document, get the document type instance
if instance and isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)):
doc_field = instance
# now get the subdocument
doc_field = getattr(doc_field, field_part, doc_field)
# We may need to cast to the correct type eg. ListField(EmbeddedDocumentField)
if isinstance(doc_field, ListField):
doc_field = getattr(doc_field, "field", doc_field)
if isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)):
instance = getattr(doc_field, "document_type", False)
if instance and isinstance(doc_field, (EmbeddedDocumentField,
GenericEmbeddedDocumentField)):
distinct = [instance(**doc) for doc in distinct] distinct = [instance(**doc) for doc in distinct]
return distinct return distinct
@@ -653,6 +809,7 @@ class BaseQuerySet(object):
.. versionchanged:: 0.5 - Added subfield support .. versionchanged:: 0.5 - Added subfield support
""" """
fields = dict([(f, QueryFieldList.ONLY) for f in fields]) fields = dict([(f, QueryFieldList.ONLY) for f in fields])
self.only_fields = fields.keys()
return self.fields(True, **fields) return self.fields(True, **fields)
def exclude(self, *fields): def exclude(self, *fields):
@@ -709,7 +866,8 @@ class BaseQuerySet(object):
for value, group in itertools.groupby(fields, lambda x: x[1]): for value, group in itertools.groupby(fields, lambda x: x[1]):
fields = [field for field, value in group] fields = [field for field, value in group]
fields = queryset._fields_to_dbfields(fields) fields = queryset._fields_to_dbfields(fields)
queryset._loaded_fields += QueryFieldList(fields, value=value, _only_called=_only_called) queryset._loaded_fields += QueryFieldList(
fields, value=value, _only_called=_only_called)
return queryset return queryset
@@ -830,6 +988,13 @@ class BaseQuerySet(object):
queryset._as_pymongo_coerce = coerce_types queryset._as_pymongo_coerce = coerce_types
return queryset return queryset
def max_time_ms(self, ms):
"""Wait `ms` milliseconds before killing the query on the server
:param ms: the number of milliseconds before killing the query on the server
"""
return self._chainable_method("max_time_ms", ms)
# JSON Helpers # JSON Helpers
def to_json(self, *args, **kwargs): def to_json(self, *args, **kwargs):
@@ -839,10 +1004,35 @@ class BaseQuerySet(object):
def from_json(self, json_data): def from_json(self, json_data):
"""Converts json data to unsaved objects""" """Converts json data to unsaved objects"""
son_data = json_util.loads(json_data) son_data = json_util.loads(json_data)
return [self._document._from_son(data) for data in son_data] return [self._document._from_son(data, only_fields=self.only_fields) for data in son_data]
def aggregate(self, *pipeline, **kwargs):
"""
Perform a aggregate function based in your queryset params
:param pipeline: list of aggregation commands,\
see: http://docs.mongodb.org/manual/core/aggregation-pipeline/
.. versionadded:: 0.9
"""
initial_pipeline = []
if self._query:
initial_pipeline.append({'$match': self._query})
if self._ordering:
initial_pipeline.append({'$sort': dict(self._ordering)})
if self._limit is not None:
initial_pipeline.append({'$limit': self._limit})
if self._skip is not None:
initial_pipeline.append({'$skip': self._skip})
pipeline = initial_pipeline + list(pipeline)
return self._collection.aggregate(pipeline, cursor={}, **kwargs)
# JS functionality # JS functionality
def map_reduce(self, map_f, reduce_f, output, finalize_f=None, limit=None, def map_reduce(self, map_f, reduce_f, output, finalize_f=None, limit=None,
scope=None): scope=None):
"""Perform a map/reduce query using the current query spec """Perform a map/reduce query using the current query spec
@@ -923,8 +1113,36 @@ class BaseQuerySet(object):
map_reduce_function = 'inline_map_reduce' map_reduce_function = 'inline_map_reduce'
else: else:
map_reduce_function = 'map_reduce' map_reduce_function = 'map_reduce'
if isinstance(output, basestring):
mr_args['out'] = output mr_args['out'] = output
elif isinstance(output, dict):
ordered_output = []
for part in ('replace', 'merge', 'reduce'):
value = output.get(part)
if value:
ordered_output.append((part, value))
break
else:
raise OperationError("actionData not specified for output")
db_alias = output.get('db_alias')
remaing_args = ['db', 'sharded', 'nonAtomic']
if db_alias:
ordered_output.append(('db', get_db(db_alias).name))
del remaing_args[0]
for part in remaing_args:
value = output.get(part)
if value:
ordered_output.append((part, value))
mr_args['out'] = SON(ordered_output)
results = getattr(queryset._collection, map_reduce_function)( results = getattr(queryset._collection, map_reduce_function)(
map_f, reduce_f, **mr_args) map_f, reduce_f, **mr_args)
@@ -1138,7 +1356,8 @@ class BaseQuerySet(object):
if self._as_pymongo: if self._as_pymongo:
return self._get_as_pymongo(raw_doc) return self._get_as_pymongo(raw_doc)
doc = self._document._from_son(raw_doc, doc = self._document._from_son(raw_doc,
_auto_dereference=self._auto_dereference) _auto_dereference=self._auto_dereference, only_fields=self.only_fields)
if self._scalar: if self._scalar:
return self._get_scalar(doc) return self._get_scalar(doc)
@@ -1147,6 +1366,7 @@ class BaseQuerySet(object):
def rewind(self): def rewind(self):
"""Rewind the cursor to its unevaluated state. """Rewind the cursor to its unevaluated state.
.. versionadded:: 0.3 .. versionadded:: 0.3
""" """
self._iter = False self._iter = False
@@ -1173,6 +1393,13 @@ class BaseQuerySet(object):
cursor_args['slave_okay'] = self._slave_okay cursor_args['slave_okay'] = self._slave_okay
if self._loaded_fields: if self._loaded_fields:
cursor_args['fields'] = self._loaded_fields.as_dict() cursor_args['fields'] = self._loaded_fields.as_dict()
if self._search_text:
if 'fields' not in cursor_args:
cursor_args['fields'] = {}
cursor_args['fields']['_text_score'] = {'$meta': "textScore"}
return cursor_args return cursor_args
@property @property
@@ -1189,8 +1416,9 @@ class BaseQuerySet(object):
if self._ordering: if self._ordering:
# Apply query ordering # Apply query ordering
self._cursor_obj.sort(self._ordering) self._cursor_obj.sort(self._ordering)
elif self._document._meta['ordering']: elif self._ordering is None and self._document._meta['ordering']:
# Otherwise, apply the ordering from the document model # Otherwise, apply the ordering from the document model, unless
# it's been explicitly cleared via order_by with no arguments
order = self._get_order_by(self._document._meta['ordering']) order = self._get_order_by(self._document._meta['ordering'])
self._cursor_obj.sort(order) self._cursor_obj.sort(order)
@@ -1213,7 +1441,10 @@ class BaseQuerySet(object):
def _query(self): def _query(self):
if self._mongo_query is None: if self._mongo_query is None:
self._mongo_query = self._query_obj.to_query(self._document) self._mongo_query = self._query_obj.to_query(self._document)
if self._class_check: if self._class_check and self._initial_query:
if "_cls" in self._mongo_query:
self._mongo_query = {"$and": [self._initial_query, self._mongo_query]}
else:
self._mongo_query.update(self._initial_query) self._mongo_query.update(self._initial_query)
return self._mongo_query return self._mongo_query
@@ -1380,6 +1611,11 @@ class BaseQuerySet(object):
for key in keys: for key in keys:
if not key: if not key:
continue continue
if key == '$text_score':
key_list.append(('_text_score', {'$meta': "textScore"}))
continue
direction = pymongo.ASCENDING direction = pymongo.ASCENDING
if key[0] == '-': if key[0] == '-':
direction = pymongo.DESCENDING direction = pymongo.DESCENDING
@@ -1392,7 +1628,7 @@ class BaseQuerySet(object):
pass pass
key_list.append((key, direction)) key_list.append((key, direction))
if self._cursor_obj: if self._cursor_obj and key_list:
self._cursor_obj.sort(key_list) self._cursor_obj.sort(key_list)
return key_list return key_list
@@ -1450,6 +1686,7 @@ class BaseQuerySet(object):
# type of this field and use the corresponding # type of this field and use the corresponding
# .to_python(...) # .to_python(...)
from mongoengine.fields import EmbeddedDocumentField from mongoengine.fields import EmbeddedDocumentField
obj = self._document obj = self._document
for chunk in path.split('.'): for chunk in path.split('.'):
obj = getattr(obj, chunk, None) obj = getattr(obj, chunk, None)
@@ -1460,6 +1697,7 @@ class BaseQuerySet(object):
if obj and data is not None: if obj and data is not None:
data = obj.to_python(data) data = obj.to_python(data)
return data return data
return clean(row) return clean(row)
def _sub_js_fields(self, code): def _sub_js_fields(self, code):
@@ -1468,6 +1706,7 @@ class BaseQuerySet(object):
substituted for the MongoDB name of the field (specified using the substituted for the MongoDB name of the field (specified using the
:attr:`name` keyword argument in a field's constructor). :attr:`name` keyword argument in a field's constructor).
""" """
def field_sub(match): def field_sub(match):
# Extract just the field name, and look up the field objects # Extract just the field name, and look up the field objects
field_name = match.group(1).split('.') field_name = match.group(1).split('.')
@@ -1487,6 +1726,13 @@ class BaseQuerySet(object):
code) code)
return code return code
def _chainable_method(self, method_name, val):
queryset = self.clone()
method = getattr(queryset._cursor, method_name)
method(val)
setattr(queryset, "_" + method_name, val)
return queryset
# Deprecated # Deprecated
def ensure_index(self, **kwargs): def ensure_index(self, **kwargs):
"""Deprecated use :func:`Document.ensure_index`""" """Deprecated use :func:`Document.ensure_index`"""

View File

@@ -94,7 +94,7 @@ class QuerySet(BaseQuerySet):
except StopIteration: except StopIteration:
self._has_more = False self._has_more = False
def count(self, with_limit_and_skip=True): def count(self, with_limit_and_skip=False):
"""Count the selected elements in the query. """Count the selected elements in the query.
:param with_limit_and_skip (optional): take any :meth:`limit` or :param with_limit_and_skip (optional): take any :meth:`limit` or
@@ -155,3 +155,10 @@ class QuerySetNoCache(BaseQuerySet):
queryset = self.clone() queryset = self.clone()
queryset.rewind() queryset.rewind()
return queryset return queryset
class QuerySetNoDeRef(QuerySet):
"""Special no_dereference QuerySet"""
def __dereference(items, max_depth=1, instance=None, name=None):
return items

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
import pymongo import pymongo
from bson import SON from bson import SON
from mongoengine.connection import get_connection
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.errors import InvalidQueryError, LookUpError from mongoengine.errors import InvalidQueryError, LookUpError
@@ -10,7 +11,7 @@ __all__ = ('query', 'update')
COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
'all', 'size', 'exists', 'not') 'all', 'size', 'exists', 'not', 'elemMatch', 'type')
GEO_OPERATORS = ('within_distance', 'within_spherical_distance', GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
'within_box', 'within_polygon', 'near', 'near_sphere', 'within_box', 'within_polygon', 'near', 'near_sphere',
'max_distance', 'geo_within', 'geo_within_box', 'max_distance', 'geo_within', 'geo_within_box',
@@ -25,7 +26,7 @@ MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push', UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push',
'push_all', 'pull', 'pull_all', 'add_to_set', 'push_all', 'pull', 'pull_all', 'add_to_set',
'set_on_insert') 'set_on_insert', 'min', 'max')
def query(_doc_cls=None, _field_operation=False, **query): def query(_doc_cls=None, _field_operation=False, **query):
@@ -38,7 +39,7 @@ def query(_doc_cls=None, _field_operation=False, **query):
mongo_query.update(value) mongo_query.update(value)
continue continue
parts = key.split('__') parts = key.rsplit('__')
indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()] indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()]
parts = [part for part in parts if not part.isdigit()] parts = [part for part in parts if not part.isdigit()]
# Check for an operator and transform to mongo-style if there is # Check for an operator and transform to mongo-style if there is
@@ -59,14 +60,20 @@ def query(_doc_cls=None, _field_operation=False, **query):
raise InvalidQueryError(e) raise InvalidQueryError(e)
parts = [] parts = []
CachedReferenceField = _import_class('CachedReferenceField')
cleaned_fields = [] cleaned_fields = []
for field in fields: for field in fields:
append_field = True append_field = True
if isinstance(field, basestring): if isinstance(field, basestring):
parts.append(field) parts.append(field)
append_field = False append_field = False
# is last and CachedReferenceField
elif isinstance(field, CachedReferenceField) and fields[-1] == field:
parts.append('%s._id' % field.db_field)
else: else:
parts.append(field.db_field) parts.append(field.db_field)
if append_field: if append_field:
cleaned_fields.append(field) cleaned_fields.append(field)
@@ -85,6 +92,10 @@ def query(_doc_cls=None, _field_operation=False, **query):
value = field value = field
else: else:
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
if isinstance(field, CachedReferenceField) and value:
value = value['_id']
elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict): elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
# 'in', 'nin' and 'all' require a list of values # 'in', 'nin' and 'all' require a list of values
value = [field.prepare_query_value(op, v) for v in value] value = [field.prepare_query_value(op, v) for v in value]
@@ -94,7 +105,7 @@ def query(_doc_cls=None, _field_operation=False, **query):
if op in GEO_OPERATORS: if op in GEO_OPERATORS:
value = _geo_operator(field, op, value) value = _geo_operator(field, op, value)
elif op in CUSTOM_OPERATORS: elif op in CUSTOM_OPERATORS:
if op == 'match': if op in ('elem_match', 'match'):
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
value = {"$elemMatch": value} value = {"$elemMatch": value}
else: else:
@@ -115,14 +126,28 @@ def query(_doc_cls=None, _field_operation=False, **query):
if key in mongo_query and isinstance(mongo_query[key], dict): if key in mongo_query and isinstance(mongo_query[key], dict):
mongo_query[key].update(value) mongo_query[key].update(value)
# $maxDistance needs to come last - convert to SON # $maxDistance needs to come last - convert to SON
if '$maxDistance' in mongo_query[key]:
value_dict = mongo_query[key] value_dict = mongo_query[key]
if ('$maxDistance' in value_dict and '$near' in value_dict):
value_son = SON() value_son = SON()
if isinstance(value_dict['$near'], dict):
for k, v in value_dict.iteritems():
if k == '$maxDistance':
continue
value_son[k] = v
if (get_connection().max_wire_version <= 1):
value_son['$maxDistance'] = value_dict[
'$maxDistance']
else:
value_son['$near'] = SON(value_son['$near'])
value_son['$near'][
'$maxDistance'] = value_dict['$maxDistance']
else:
for k, v in value_dict.iteritems(): for k, v in value_dict.iteritems():
if k == '$maxDistance': if k == '$maxDistance':
continue continue
value_son[k] = v value_son[k] = v
value_son['$maxDistance'] = value_dict['$maxDistance'] value_son['$maxDistance'] = value_dict['$maxDistance']
mongo_query[key] = value_son mongo_query[key] = value_son
else: else:
# Store for manually merging later # Store for manually merging later
@@ -135,7 +160,7 @@ def query(_doc_cls=None, _field_operation=False, **query):
if isinstance(v, list): if isinstance(v, list):
value = [{k: val} for val in v] value = [{k: val} for val in v]
if '$and' in mongo_query.keys(): if '$and' in mongo_query.keys():
mongo_query['$and'].append(value) mongo_query['$and'].extend(value)
else: else:
mongo_query['$and'] = value mongo_query['$and'] = value
@@ -151,6 +176,9 @@ def update(_doc_cls=None, **update):
mongo_update.update(value) mongo_update.update(value)
continue continue
parts = key.split('__') parts = key.split('__')
# if there is no operator, default to "set"
if len(parts) < 3 and parts[0] not in UPDATE_OPERATORS:
parts.insert(0, 'set')
# Check for an operator and transform to mongo-style if there is # Check for an operator and transform to mongo-style if there is
op = None op = None
if parts[0] in UPDATE_OPERATORS: if parts[0] in UPDATE_OPERATORS:
@@ -248,7 +276,8 @@ def update(_doc_cls=None, **update):
if ListField in field_classes: if ListField in field_classes:
# Join all fields via dot notation to the last ListField # Join all fields via dot notation to the last ListField
# Then process as normal # Then process as normal
last_listField = len(cleaned_fields) - field_classes.index(ListField) last_listField = len(
cleaned_fields) - field_classes.index(ListField)
key = ".".join(parts[:last_listField]) key = ".".join(parts[:last_listField])
parts = parts[last_listField:] parts = parts[last_listField:]
parts.insert(0, key) parts.insert(0, key)

View File

@@ -1,8 +1,9 @@
import copy import copy
from mongoengine.errors import InvalidQueryError from itertools import product
from mongoengine.python_support import product, reduce from functools import reduce
from mongoengine.errors import InvalidQueryError
from mongoengine.queryset import transform from mongoengine.queryset import transform
__all__ = ('Q',) __all__ = ('Q',)
@@ -28,7 +29,7 @@ class DuplicateQueryConditionsError(InvalidQueryError):
class SimplificationVisitor(QNodeVisitor): class SimplificationVisitor(QNodeVisitor):
"""Simplifies query trees by combinging unnecessary 'and' connection nodes """Simplifies query trees by combining unnecessary 'and' connection nodes
into a single Q-object. into a single Q-object.
""" """

View File

@@ -1 +1,2 @@
pymongo pymongo>=2.7.1
nose

View File

@@ -41,9 +41,11 @@ CLASSIFIERS = [
"Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.1",
"Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
'Topic :: Database', 'Topic :: Database',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
@@ -51,12 +53,15 @@ CLASSIFIERS = [
extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])} extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])}
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
extra_opts['use_2to3'] = True extra_opts['use_2to3'] = True
extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'django>=1.5.1'] extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'Pillow>=2.0.0', 'django>=1.5.1']
if "test" in sys.argv or "nosetests" in sys.argv: if "test" in sys.argv or "nosetests" in sys.argv:
extra_opts['packages'] = find_packages() extra_opts['packages'] = find_packages()
extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]} extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]}
else: else:
extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'PIL', 'jinja2>=2.6', 'python-dateutil'] extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'Pillow>=2.0.0', 'jinja2>=2.6', 'python-dateutil']
if sys.version_info[0] == 2 and sys.version_info[1] == 6:
extra_opts['tests_require'].append('unittest2')
setup(name='mongoengine', setup(name='mongoengine',
version=VERSION, version=VERSION,
@@ -72,7 +77,7 @@ setup(name='mongoengine',
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
platforms=['any'], platforms=['any'],
classifiers=CLASSIFIERS, classifiers=CLASSIFIERS,
install_requires=['pymongo>=2.5'], install_requires=['pymongo>=2.7.1'],
test_suite='nose.collector', test_suite='nose.collector',
**extra_opts **extra_opts
) )

View File

@@ -36,9 +36,9 @@ class ClassMethodsTest(unittest.TestCase):
def test_definition(self): def test_definition(self):
"""Ensure that document may be defined using fields. """Ensure that document may be defined using fields.
""" """
self.assertEqual(['age', 'id', 'name'], self.assertEqual(['_cls', 'age', 'id', 'name'],
sorted(self.Person._fields.keys())) sorted(self.Person._fields.keys()))
self.assertEqual(["IntField", "ObjectIdField", "StringField"], self.assertEqual(["IntField", "ObjectIdField", "StringField", "StringField"],
sorted([x.__class__.__name__ for x in sorted([x.__class__.__name__ for x in
self.Person._fields.values()])) self.Person._fields.values()]))

View File

@@ -207,22 +207,21 @@ class DeltaTest(unittest.TestCase):
doc.embedded_field.list_field[2].string_field = 'hello world' doc.embedded_field.list_field[2].string_field = 'hello world'
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2] doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
self.assertEqual(doc._get_changed_fields(), self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field']) ['embedded_field.list_field.2'])
self.assertEqual(doc.embedded_field._delta(), ({ self.assertEqual(doc.embedded_field._delta(), ({'list_field.2': {
'list_field': ['1', 2, {
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}]}, {}))
self.assertEqual(doc._delta(), ({
'embedded_field.list_field': ['1', 2, {
'_cls': 'Embedded', '_cls': 'Embedded',
'string_field': 'hello world', 'string_field': 'hello world',
'int_field': 1, 'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}], 'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}} 'dict_field': {'hello': 'world'}}
]}, {})) }, {}))
self.assertEqual(doc._delta(), ({'embedded_field.list_field.2': {
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}
}, {}))
doc.save() doc.save()
doc = doc.reload(10) doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field, self.assertEqual(doc.embedded_field.list_field[2].string_field,
@@ -253,7 +252,7 @@ class DeltaTest(unittest.TestCase):
del(doc.embedded_field.list_field[2].list_field[2]['hello']) del(doc.embedded_field.list_field[2].list_field[2]['hello'])
self.assertEqual(doc._delta(), self.assertEqual(doc._delta(),
({'embedded_field.list_field.2.list_field': [1, 2, {}]}, {})) ({}, {'embedded_field.list_field.2.list_field.2.hello': 1}))
doc.save() doc.save()
doc = doc.reload(10) doc = doc.reload(10)
@@ -548,22 +547,21 @@ class DeltaTest(unittest.TestCase):
doc.embedded_field.list_field[2].string_field = 'hello world' doc.embedded_field.list_field[2].string_field = 'hello world'
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2] doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
self.assertEqual(doc._get_changed_fields(), self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field']) ['db_embedded_field.db_list_field.2'])
self.assertEqual(doc.embedded_field._delta(), ({ self.assertEqual(doc.embedded_field._delta(), ({'db_list_field.2': {
'db_list_field': ['1', 2, {
'_cls': 'Embedded', '_cls': 'Embedded',
'db_string_field': 'hello world', 'db_string_field': 'hello world',
'db_int_field': 1, 'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}], 'db_list_field': ['1', 2, {'hello': 'world'}],
'db_dict_field': {'hello': 'world'}}]}, {})) 'db_dict_field': {'hello': 'world'}}}, {}))
self.assertEqual(doc._delta(), ({ self.assertEqual(doc._delta(), ({
'db_embedded_field.db_list_field': ['1', 2, { 'db_embedded_field.db_list_field.2': {
'_cls': 'Embedded', '_cls': 'Embedded',
'db_string_field': 'hello world', 'db_string_field': 'hello world',
'db_int_field': 1, 'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}], 'db_list_field': ['1', 2, {'hello': 'world'}],
'db_dict_field': {'hello': 'world'}} 'db_dict_field': {'hello': 'world'}}
]}, {})) }, {}))
doc.save() doc.save()
doc = doc.reload(10) doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field, self.assertEqual(doc.embedded_field.list_field[2].string_field,
@@ -594,8 +592,7 @@ class DeltaTest(unittest.TestCase):
del(doc.embedded_field.list_field[2].list_field[2]['hello']) del(doc.embedded_field.list_field[2].list_field[2]['hello'])
self.assertEqual(doc._delta(), self.assertEqual(doc._delta(),
({'db_embedded_field.db_list_field.2.db_list_field': ({}, {'db_embedded_field.db_list_field.2.db_list_field.2.hello': 1}))
[1, 2, {}]}, {}))
doc.save() doc.save()
doc = doc.reload(10) doc = doc.reload(10)
@@ -735,5 +732,47 @@ class DeltaTest(unittest.TestCase):
mydoc._clear_changed_fields() mydoc._clear_changed_fields()
self.assertEqual([], mydoc._get_changed_fields()) self.assertEqual([], mydoc._get_changed_fields())
def test_referenced_object_changed_attributes(self):
"""Ensures that when you save a new reference to a field, the referenced object isn't altered"""
class Organization(Document):
name = StringField()
class User(Document):
name = StringField()
org = ReferenceField('Organization', required=True)
Organization.drop_collection()
User.drop_collection()
org1 = Organization(name='Org 1')
org1.save()
org2 = Organization(name='Org 2')
org2.save()
user = User(name='Fred', org=org1)
user.save()
org1.reload()
org2.reload()
user.reload()
self.assertEqual(org1.name, 'Org 1')
self.assertEqual(org2.name, 'Org 2')
self.assertEqual(user.name, 'Fred')
user.name = 'Harold'
user.org = org2
org2.name = 'New Org 2'
self.assertEqual(org2.name, 'New Org 2')
user.save()
org2.save()
self.assertEqual(org2.name, 'New Org 2')
org2.reload()
self.assertEqual(org2.name, 'New Org 2')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -81,6 +81,13 @@ class DynamicTest(unittest.TestCase):
obj = collection.find_one() obj = collection.find_one()
self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'name']) self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'name'])
def test_reload_after_unsetting(self):
p = self.Person()
p.misc = 22
p.save()
p.update(unset__misc=1)
p.reload()
def test_dynamic_document_queries(self): def test_dynamic_document_queries(self):
"""Ensure we can query dynamic fields""" """Ensure we can query dynamic fields"""
p = self.Person() p = self.Person()
@@ -292,6 +299,59 @@ class DynamicTest(unittest.TestCase):
person.save() person.save()
self.assertEqual(Person.objects.first().age, 35) self.assertEqual(Person.objects.first().age, 35)
def test_dynamic_embedded_works_with_only(self):
"""Ensure custom fieldnames on a dynamic embedded document are found by qs.only()"""
class Address(DynamicEmbeddedDocument):
city = StringField()
class Person(DynamicDocument):
address = EmbeddedDocumentField(Address)
Person.drop_collection()
Person(name="Eric", address=Address(city="San Francisco", street_number="1337")).save()
self.assertEqual(Person.objects.first().address.street_number, '1337')
self.assertEqual(Person.objects.only('address__street_number').first().address.street_number, '1337')
def test_dynamic_and_embedded_dict_access(self):
"""Ensure embedded dynamic documents work with dict[] style access"""
class Address(EmbeddedDocument):
city = StringField()
class Person(DynamicDocument):
name = StringField()
Person.drop_collection()
Person(name="Ross", address=Address(city="London")).save()
person = Person.objects.first()
person.attrval = "This works"
person["phone"] = "555-1212" # but this should too
# Same thing two levels deep
person["address"]["city"] = "Lundenne"
person.save()
self.assertEqual(Person.objects.first().address.city, "Lundenne")
self.assertEqual(Person.objects.first().phone, "555-1212")
person = Person.objects.first()
person.address = Address(city="Londinium")
person.save()
self.assertEqual(Person.objects.first().address.city, "Londinium")
person = Person.objects.first()
person["age"] = 35
person.save()
self.assertEqual(Person.objects.first().age, 35)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -18,7 +18,7 @@ __all__ = ("IndexesTest", )
class IndexesTest(unittest.TestCase): class IndexesTest(unittest.TestCase):
def setUp(self): def setUp(self):
connect(db='mongoenginetest') self.connection = connect(db='mongoenginetest')
self.db = get_db() self.db = get_db()
class Person(Document): class Person(Document):
@@ -175,6 +175,16 @@ class IndexesTest(unittest.TestCase):
info = A._get_collection().index_information() info = A._get_collection().index_information()
self.assertEqual(len(info.keys()), 2) self.assertEqual(len(info.keys()), 2)
class B(A):
c = StringField()
d = StringField()
meta = {
'indexes': [{'fields': ['c']}, {'fields': ['d'], 'cls': True}],
'allow_inheritance': True
}
self.assertEqual([('c', 1)], B._meta['index_specs'][1]['fields'])
self.assertEqual([('_cls', 1), ('d', 1)], B._meta['index_specs'][2]['fields'])
def test_build_index_spec_is_not_destructive(self): def test_build_index_spec_is_not_destructive(self):
class MyDoc(Document): class MyDoc(Document):
@@ -485,6 +495,9 @@ class IndexesTest(unittest.TestCase):
self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10) self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10)
if pymongo.version >= '2.8':
self.assertEqual(BlogPost.objects.hint('tags').count(), 10)
else:
def invalid_index(): def invalid_index():
BlogPost.objects.hint('tags') BlogPost.objects.hint('tags')
self.assertRaises(TypeError, invalid_index) self.assertRaises(TypeError, invalid_index)
@@ -567,6 +580,38 @@ class IndexesTest(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
def test_unique_embedded_document_in_list(self):
"""
Ensure that the uniqueness constraints are applied to fields in
embedded documents, even when the embedded documents in in a
list field.
"""
class SubDocument(EmbeddedDocument):
year = IntField(db_field='yr')
slug = StringField(unique=True)
class BlogPost(Document):
title = StringField()
subs = ListField(EmbeddedDocumentField(SubDocument))
BlogPost.drop_collection()
post1 = BlogPost(
title='test1', subs=[
SubDocument(year=2009, slug='conflict'),
SubDocument(year=2009, slug='conflict')
]
)
post1.save()
post2 = BlogPost(
title='test2', subs=[SubDocument(year=2014, slug='conflict')]
)
self.assertRaises(NotUniqueError, post2.save)
BlogPost.drop_collection()
def test_unique_with_embedded_document_and_embedded_unique(self): def test_unique_with_embedded_document_and_embedded_unique(self):
"""Ensure that uniqueness constraints are applied to fields on """Ensure that uniqueness constraints are applied to fields on
embedded documents. And work with unique_with as well. embedded documents. And work with unique_with as well.
@@ -727,5 +772,59 @@ class IndexesTest(unittest.TestCase):
report.to_mongo()) report.to_mongo())
self.assertEqual(report, Report.objects.get(pk=my_key)) self.assertEqual(report, Report.objects.get(pk=my_key))
def test_string_indexes(self):
class MyDoc(Document):
provider_ids = DictField()
meta = {
"indexes": ["provider_ids.foo", "provider_ids.bar"],
}
info = MyDoc.objects._collection.index_information()
info = [value['key'] for key, value in info.iteritems()]
self.assertTrue([('provider_ids.foo', 1)] in info)
self.assertTrue([('provider_ids.bar', 1)] in info)
def test_text_indexes(self):
class Book(Document):
title = DictField()
meta = {
"indexes": ["$title"],
}
indexes = Book.objects._collection.index_information()
self.assertTrue("title_text" in indexes)
key = indexes["title_text"]["key"]
self.assertTrue(('_fts', 'text') in key)
def test_indexes_after_database_drop(self):
"""
Test to ensure that indexes are re-created on a collection even
after the database has been dropped.
Issue #812
"""
class BlogPost(Document):
title = StringField()
slug = StringField(unique=True)
BlogPost.drop_collection()
# Create Post #1
post1 = BlogPost(title='test1', slug='test')
post1.save()
# Drop the Database
self.connection.drop_database(BlogPost._get_db().name)
# Re-create Post #1
post1 = BlogPost(title='test1', slug='test')
post1.save()
# Create Post #2
post2 = BlogPost(title='test2', slug='test')
self.assertRaises(NotUniqueError, post2.save)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -163,7 +163,7 @@ class InheritanceTest(unittest.TestCase):
class Employee(Person): class Employee(Person):
salary = IntField() salary = IntField()
self.assertEqual(['age', 'id', 'name', 'salary'], self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
sorted(Employee._fields.keys())) sorted(Employee._fields.keys()))
self.assertEqual(Employee._get_collection_name(), self.assertEqual(Employee._get_collection_name(),
Person._get_collection_name()) Person._get_collection_name())
@@ -180,7 +180,7 @@ class InheritanceTest(unittest.TestCase):
class Employee(Person): class Employee(Person):
salary = IntField() salary = IntField()
self.assertEqual(['age', 'id', 'name', 'salary'], self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
sorted(Employee._fields.keys())) sorted(Employee._fields.keys()))
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(), self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
['_cls', 'name', 'age']) ['_cls', 'name', 'age'])
@@ -397,6 +397,16 @@ class InheritanceTest(unittest.TestCase):
meta = {'abstract': True} meta = {'abstract': True}
self.assertRaises(ValueError, create_bad_abstract) self.assertRaises(ValueError, create_bad_abstract)
def test_abstract_embedded_documents(self):
# 789: EmbeddedDocument shouldn't inherit abstract
class A(EmbeddedDocument):
meta = {"abstract": True}
class B(A):
pass
self.assertFalse(B._meta["abstract"])
def test_inherited_collections(self): def test_inherited_collections(self):
"""Ensure that subclassed documents don't override parents' """Ensure that subclassed documents don't override parents'
collections collections

View File

@@ -9,13 +9,13 @@ import unittest
import uuid import uuid
from datetime import datetime from datetime import datetime
from bson import DBRef from bson import DBRef, ObjectId
from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest,
PickleDyanmicEmbedded, PickleDynamicTest) PickleDyanmicEmbedded, PickleDynamicTest)
from mongoengine import * from mongoengine import *
from mongoengine.errors import (NotRegistered, InvalidDocumentError, from mongoengine.errors import (NotRegistered, InvalidDocumentError,
InvalidQueryError) InvalidQueryError, NotUniqueError)
from mongoengine.queryset import NULLIFY, Q from mongoengine.queryset import NULLIFY, Q
from mongoengine.connection import get_db from mongoengine.connection import get_db
from mongoengine.base import get_document from mongoengine.base import get_document
@@ -34,15 +34,21 @@ class InstanceTest(unittest.TestCase):
connect(db='mongoenginetest') connect(db='mongoenginetest')
self.db = get_db() self.db = get_db()
class Job(EmbeddedDocument):
name = StringField()
years = IntField()
class Person(Document): class Person(Document):
name = StringField() name = StringField()
age = IntField() age = IntField()
job = EmbeddedDocumentField(Job)
non_field = True non_field = True
meta = {"allow_inheritance": True} meta = {"allow_inheritance": True}
self.Person = Person self.Person = Person
self.Job = Job
def tearDown(self): def tearDown(self):
for collection in self.db.collection_names(): for collection in self.db.collection_names():
@@ -50,6 +56,11 @@ class InstanceTest(unittest.TestCase):
continue continue
self.db.drop_collection(collection) self.db.drop_collection(collection)
def assertDbEqual(self, docs):
self.assertEqual(
list(self.Person._get_collection().find().sort("id")),
sorted(docs, key=lambda doc: doc["_id"]))
def test_capped_collection(self): def test_capped_collection(self):
"""Ensure that capped collections work properly. """Ensure that capped collections work properly.
""" """
@@ -57,7 +68,7 @@ class InstanceTest(unittest.TestCase):
date = DateTimeField(default=datetime.now) date = DateTimeField(default=datetime.now)
meta = { meta = {
'max_documents': 10, 'max_documents': 10,
'max_size': 90000, 'max_size': 4096,
} }
Log.drop_collection() Log.drop_collection()
@@ -75,7 +86,7 @@ class InstanceTest(unittest.TestCase):
options = Log.objects._collection.options() options = Log.objects._collection.options()
self.assertEqual(options['capped'], True) self.assertEqual(options['capped'], True)
self.assertEqual(options['max'], 10) self.assertEqual(options['max'], 10)
self.assertEqual(options['size'], 90000) self.assertTrue(options['size'] >= 4096)
# Check that the document cannot be redefined with different options # Check that the document cannot be redefined with different options
def recreate_log_document(): def recreate_log_document():
@@ -103,6 +114,19 @@ class InstanceTest(unittest.TestCase):
self.assertEqual('<Article: привет мир>', repr(doc)) self.assertEqual('<Article: привет мир>', repr(doc))
def test_repr_none(self):
"""Ensure None values handled correctly
"""
class Article(Document):
title = StringField()
def __str__(self):
return None
doc = Article(title=u'привет мир')
self.assertEqual('<Article: None>', repr(doc))
def test_queryset_resurrects_dropped_collection(self): def test_queryset_resurrects_dropped_collection(self):
self.Person.drop_collection() self.Person.drop_collection()
@@ -122,10 +146,18 @@ class InstanceTest(unittest.TestCase):
""" """
class Animal(Document): class Animal(Document):
meta = {'allow_inheritance': True} meta = {'allow_inheritance': True}
class Fish(Animal): pass
class Mammal(Animal): pass class Fish(Animal):
class Dog(Mammal): pass pass
class Human(Mammal): pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
class Zoo(Document): class Zoo(Document):
animals = ListField(ReferenceField(Animal)) animals = ListField(ReferenceField(Animal))
@@ -353,6 +385,14 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(person.name, "Test User") self.assertEqual(person.name, "Test User")
self.assertEqual(person.age, 20) self.assertEqual(person.age, 20)
person.reload('age')
self.assertEqual(person.name, "Test User")
self.assertEqual(person.age, 21)
person.reload()
self.assertEqual(person.name, "Mr Test User")
self.assertEqual(person.age, 21)
person.reload() person.reload()
self.assertEqual(person.name, "Mr Test User") self.assertEqual(person.name, "Mr Test User")
self.assertEqual(person.age, 21) self.assertEqual(person.age, 21)
@@ -398,10 +438,11 @@ class InstanceTest(unittest.TestCase):
doc.embedded_field.dict_field['woot'] = "woot" doc.embedded_field.dict_field['woot'] = "woot"
self.assertEqual(doc._get_changed_fields(), [ self.assertEqual(doc._get_changed_fields(), [
'list_field', 'dict_field', 'embedded_field.list_field', 'list_field', 'dict_field.woot', 'embedded_field.list_field',
'embedded_field.dict_field']) 'embedded_field.dict_field.woot'])
doc.save() doc.save()
self.assertEqual(len(doc.list_field), 4)
doc = doc.reload(10) doc = doc.reload(10)
self.assertEqual(doc._get_changed_fields(), []) self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(len(doc.list_field), 4) self.assertEqual(len(doc.list_field), 4)
@@ -409,6 +450,16 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(len(doc.embedded_field.list_field), 4) self.assertEqual(len(doc.embedded_field.list_field), 4)
self.assertEqual(len(doc.embedded_field.dict_field), 2) self.assertEqual(len(doc.embedded_field.dict_field), 2)
doc.list_field.append(1)
doc.save()
doc.dict_field['extra'] = 1
doc = doc.reload(10, 'list_field')
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(len(doc.list_field), 5)
self.assertEqual(len(doc.dict_field), 3)
self.assertEqual(len(doc.embedded_field.list_field), 4)
self.assertEqual(len(doc.embedded_field.dict_field), 2)
def test_reload_doesnt_exist(self): def test_reload_doesnt_exist(self):
class Foo(Document): class Foo(Document):
pass pass
@@ -418,7 +469,7 @@ class InstanceTest(unittest.TestCase):
f.reload() f.reload()
except Foo.DoesNotExist: except Foo.DoesNotExist:
pass pass
except Exception as ex: except Exception:
self.assertFalse("Threw wrong exception") self.assertFalse("Threw wrong exception")
f.save() f.save()
@@ -427,13 +478,13 @@ class InstanceTest(unittest.TestCase):
f.reload() f.reload()
except Foo.DoesNotExist: except Foo.DoesNotExist:
pass pass
except Exception as ex: except Exception:
self.assertFalse("Threw wrong exception") self.assertFalse("Threw wrong exception")
def test_dictionary_access(self): def test_dictionary_access(self):
"""Ensure that dictionary-style field access works properly. """Ensure that dictionary-style field access works properly.
""" """
person = self.Person(name='Test User', age=30) person = self.Person(name='Test User', age=30, job=self.Job())
self.assertEqual(person['name'], 'Test User') self.assertEqual(person['name'], 'Test User')
self.assertRaises(KeyError, person.__getitem__, 'salary') self.assertRaises(KeyError, person.__getitem__, 'salary')
@@ -443,7 +494,7 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(person['name'], 'Another User') self.assertEqual(person['name'], 'Another User')
# Length = length(assigned fields + id) # Length = length(assigned fields + id)
self.assertEqual(len(person), 3) self.assertEqual(len(person), 5)
self.assertTrue('age' in person) self.assertTrue('age' in person)
person.age = None person.age = None
@@ -462,7 +513,8 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(), self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
['_cls', 'name', 'age']) ['_cls', 'name', 'age'])
self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(), self.assertEqual(
Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
['_cls', 'name', 'age', 'salary']) ['_cls', 'name', 'age', 'salary'])
def test_embedded_document_to_mongo_id(self): def test_embedded_document_to_mongo_id(self):
@@ -515,9 +567,6 @@ class InstanceTest(unittest.TestCase):
class Email(EmbeddedDocument): class Email(EmbeddedDocument):
email = EmailField() email = EmailField()
def clean(self):
print "instance:"
print self._instance
class Account(Document): class Account(Document):
email = EmbeddedDocumentField(Email) email = EmbeddedDocumentField(Email)
@@ -601,6 +650,64 @@ class InstanceTest(unittest.TestCase):
t = TestDocument(doc=TestEmbeddedDocument(x=15, y=35, z=5)) t = TestDocument(doc=TestEmbeddedDocument(x=15, y=35, z=5))
t.save(clean=False) t.save(clean=False)
def test_modify_empty(self):
doc = self.Person(name="bob", age=10).save()
self.assertRaises(
InvalidDocumentError, lambda: self.Person().modify(set__age=10))
self.assertDbEqual([dict(doc.to_mongo())])
def test_modify_invalid_query(self):
doc1 = self.Person(name="bob", age=10).save()
doc2 = self.Person(name="jim", age=20).save()
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
self.assertRaises(
InvalidQueryError,
lambda: doc1.modify(dict(id=doc2.id), set__value=20))
self.assertDbEqual(docs)
def test_modify_match_another_document(self):
doc1 = self.Person(name="bob", age=10).save()
doc2 = self.Person(name="jim", age=20).save()
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
assert not doc1.modify(dict(name=doc2.name), set__age=100)
self.assertDbEqual(docs)
def test_modify_not_exists(self):
doc1 = self.Person(name="bob", age=10).save()
doc2 = self.Person(id=ObjectId(), name="jim", age=20)
docs = [dict(doc1.to_mongo())]
assert not doc2.modify(dict(name=doc2.name), set__age=100)
self.assertDbEqual(docs)
def test_modify_update(self):
other_doc = self.Person(name="bob", age=10).save()
doc = self.Person(
name="jim", age=20, job=self.Job(name="10gen", years=3)).save()
doc_copy = doc._from_son(doc.to_mongo())
# these changes must go away
doc.name = "liza"
doc.job.name = "Google"
doc.job.years = 3
assert doc.modify(
set__age=21, set__job__name="MongoDB", unset__job__years=True)
doc_copy.age = 21
doc_copy.job.name = "MongoDB"
del doc_copy.job.years
assert doc.to_json() == doc_copy.to_json()
assert doc._get_changed_fields() == []
self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())])
def test_save(self): def test_save(self):
"""Ensure that a document may be saved in the database. """Ensure that a document may be saved in the database.
""" """
@@ -820,6 +927,80 @@ class InstanceTest(unittest.TestCase):
p1.reload() p1.reload()
self.assertEqual(p1.name, p.parent.name) self.assertEqual(p1.name, p.parent.name)
def test_save_atomicity_condition(self):
class Widget(Document):
toggle = BooleanField(default=False)
count = IntField(default=0)
save_id = UUIDField()
def flip(widget):
widget.toggle = not widget.toggle
widget.count += 1
def UUID(i):
return uuid.UUID(int=i)
Widget.drop_collection()
w1 = Widget(toggle=False, save_id=UUID(1))
# ignore save_condition on new record creation
w1.save(save_condition={'save_id': UUID(42)})
w1.reload()
self.assertFalse(w1.toggle)
self.assertEqual(w1.save_id, UUID(1))
self.assertEqual(w1.count, 0)
# mismatch in save_condition prevents save
flip(w1)
self.assertTrue(w1.toggle)
self.assertEqual(w1.count, 1)
w1.save(save_condition={'save_id': UUID(42)})
w1.reload()
self.assertFalse(w1.toggle)
self.assertEqual(w1.count, 0)
# matched save_condition allows save
flip(w1)
self.assertTrue(w1.toggle)
self.assertEqual(w1.count, 1)
w1.save(save_condition={'save_id': UUID(1)})
w1.reload()
self.assertTrue(w1.toggle)
self.assertEqual(w1.count, 1)
# save_condition can be used to ensure atomic read & updates
# i.e., prevent interleaved reads and writes from separate contexts
w2 = Widget.objects.get()
self.assertEqual(w1, w2)
old_id = w1.save_id
flip(w1)
w1.save_id = UUID(2)
w1.save(save_condition={'save_id': old_id})
w1.reload()
self.assertFalse(w1.toggle)
self.assertEqual(w1.count, 2)
flip(w2)
flip(w2)
w2.save(save_condition={'save_id': old_id})
w2.reload()
self.assertFalse(w2.toggle)
self.assertEqual(w2.count, 2)
# save_condition uses mongoengine-style operator syntax
flip(w1)
w1.save(save_condition={'count__lt': w1.count})
w1.reload()
self.assertTrue(w1.toggle)
self.assertEqual(w1.count, 3)
flip(w1)
w1.save(save_condition={'count__gte': w1.count})
w1.reload()
self.assertTrue(w1.toggle)
self.assertEqual(w1.count, 3)
def test_update(self): def test_update(self):
"""Ensure that an existing document is updated instead of be """Ensure that an existing document is updated instead of be
overwritten.""" overwritten."""
@@ -984,11 +1165,23 @@ class InstanceTest(unittest.TestCase):
self.assertRaises(OperationError, update_no_value_raises) self.assertRaises(OperationError, update_no_value_raises)
def update_no_op_raises(): def update_no_op_should_default_to_set():
person = self.Person.objects.first() person = self.Person.objects.first()
person.update(name="Dan") person.update(name="Dan")
person.reload()
return person.name
self.assertRaises(InvalidQueryError, update_no_op_raises) self.assertEqual("Dan", update_no_op_should_default_to_set())
def test_update_unique_field(self):
class Doc(Document):
name = StringField(unique=True)
doc1 = Doc(name="first").save()
doc2 = Doc(name="second").save()
self.assertRaises(NotUniqueError, lambda:
doc2.update(set__name=doc1.name))
def test_embedded_update(self): def test_embedded_update(self):
""" """
@@ -1249,7 +1442,8 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4')
def test_save_custom_pk(self): def test_save_custom_pk(self):
"""Ensure that a document may be saved with a custom _id using pk alias. """
Ensure that a document may be saved with a custom _id using pk alias.
""" """
# Create person object and save it to the database # Create person object and save it to the database
person = self.Person(name='Test User', age=30, person = self.Person(name='Test User', age=30,
@@ -1335,9 +1529,15 @@ class InstanceTest(unittest.TestCase):
p4 = Page(comments=[Comment(user=u2, comment="Heavy Metal song")]) p4 = Page(comments=[Comment(user=u2, comment="Heavy Metal song")])
p4.save() p4.save()
self.assertEqual([p1, p2], list(Page.objects.filter(comments__user=u1))) self.assertEqual(
self.assertEqual([p1, p2, p4], list(Page.objects.filter(comments__user=u2))) [p1, p2],
self.assertEqual([p1, p3], list(Page.objects.filter(comments__user=u3))) list(Page.objects.filter(comments__user=u1)))
self.assertEqual(
[p1, p2, p4],
list(Page.objects.filter(comments__user=u2)))
self.assertEqual(
[p1, p3],
list(Page.objects.filter(comments__user=u3)))
def test_save_embedded_document(self): def test_save_embedded_document(self):
"""Ensure that a document with an embedded document field may be """Ensure that a document with an embedded document field may be
@@ -1412,7 +1612,8 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(promoted_employee.age, 50) self.assertEqual(promoted_employee.age, 50)
# Ensure that the 'details' embedded object saved correctly # Ensure that the 'details' embedded object saved correctly
self.assertEqual(promoted_employee.details.position, 'Senior Developer') self.assertEqual(
promoted_employee.details.position, 'Senior Developer')
# Test removal # Test removal
promoted_employee.details = None promoted_employee.details = None
@@ -1548,7 +1749,8 @@ class InstanceTest(unittest.TestCase):
post.save() post.save()
reviewer.delete() reviewer.delete()
self.assertEqual(BlogPost.objects.count(), 1) # No effect on the BlogPost # No effect on the BlogPost
self.assertEqual(BlogPost.objects.count(), 1)
self.assertEqual(BlogPost.objects.get().reviewer, None) self.assertEqual(BlogPost.objects.get().reviewer, None)
# Delete the Person, which should lead to deletion of the BlogPost, too # Delete the Person, which should lead to deletion of the BlogPost, too
@@ -1597,8 +1799,10 @@ class InstanceTest(unittest.TestCase):
class BlogPost(Document): class BlogPost(Document):
content = StringField() content = StringField()
authors = ListField(ReferenceField(self.Person, reverse_delete_rule=CASCADE)) authors = ListField(ReferenceField(
reviewers = ListField(ReferenceField(self.Person, reverse_delete_rule=NULLIFY)) self.Person, reverse_delete_rule=CASCADE))
reviewers = ListField(ReferenceField(
self.Person, reverse_delete_rule=NULLIFY))
self.Person.drop_collection() self.Person.drop_collection()
@@ -1693,13 +1897,17 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(Bar.objects.count(), 1) # No effect on the BlogPost self.assertEqual(Bar.objects.count(), 1) # No effect on the BlogPost
self.assertEqual(Bar.objects.get().foo, None) self.assertEqual(Bar.objects.get().foo, None)
def test_invalid_reverse_delete_rules_raise_errors(self): def test_invalid_reverse_delete_rule_raise_errors(self):
def throw_invalid_document_error(): def throw_invalid_document_error():
class Blog(Document): class Blog(Document):
content = StringField() content = StringField()
authors = MapField(ReferenceField(self.Person, reverse_delete_rule=CASCADE)) authors = MapField(ReferenceField(
reviewers = DictField(field=ReferenceField(self.Person, reverse_delete_rule=NULLIFY)) self.Person, reverse_delete_rule=CASCADE))
reviewers = DictField(
field=ReferenceField(
self.Person,
reverse_delete_rule=NULLIFY))
self.assertRaises(InvalidDocumentError, throw_invalid_document_error) self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
@@ -1708,7 +1916,8 @@ class InstanceTest(unittest.TestCase):
father = ReferenceField('Person', reverse_delete_rule=DENY) father = ReferenceField('Person', reverse_delete_rule=DENY)
mother = ReferenceField('Person', reverse_delete_rule=DENY) mother = ReferenceField('Person', reverse_delete_rule=DENY)
self.assertRaises(InvalidDocumentError, throw_invalid_document_error_embedded) self.assertRaises(
InvalidDocumentError, throw_invalid_document_error_embedded)
def test_reverse_delete_rule_cascade_recurs(self): def test_reverse_delete_rule_cascade_recurs(self):
"""Ensure that a chain of documents is also deleted upon cascaded """Ensure that a chain of documents is also deleted upon cascaded
@@ -1730,16 +1939,16 @@ class InstanceTest(unittest.TestCase):
author = self.Person(name='Test User') author = self.Person(name='Test User')
author.save() author.save()
post = BlogPost(content = 'Watched some TV') post = BlogPost(content='Watched some TV')
post.author = author post.author = author
post.save() post.save()
comment = Comment(text = 'Kudos.') comment = Comment(text='Kudos.')
comment.post = post comment.post = post
comment.save() comment.save()
# Delete the Person, which should lead to deletion of the BlogPost, and, # Delete the Person, which should lead to deletion of the BlogPost,
# recursively to the Comment, too # and, recursively to the Comment, too
author.delete() author.delete()
self.assertEqual(Comment.objects.count(), 0) self.assertEqual(Comment.objects.count(), 0)
@@ -1762,7 +1971,7 @@ class InstanceTest(unittest.TestCase):
author = self.Person(name='Test User') author = self.Person(name='Test User')
author.save() author.save()
post = BlogPost(content = 'Watched some TV') post = BlogPost(content='Watched some TV')
post.author = author post.author = author
post.save() post.save()
@@ -1878,7 +2087,8 @@ class InstanceTest(unittest.TestCase):
def test_dynamic_document_pickle(self): def test_dynamic_document_pickle(self):
pickle_doc = PickleDynamicTest(name="test", number=1, string="One", lists=['1', '2']) pickle_doc = PickleDynamicTest(
name="test", number=1, string="One", lists=['1', '2'])
pickle_doc.embedded = PickleDyanmicEmbedded(foo="Bar") pickle_doc.embedded = PickleDyanmicEmbedded(foo="Bar")
pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved
@@ -1900,7 +2110,8 @@ class InstanceTest(unittest.TestCase):
pickle_doc.embedded._dynamic_fields.keys()) pickle_doc.embedded._dynamic_fields.keys())
def test_picklable_on_signals(self): def test_picklable_on_signals(self):
pickle_doc = PickleSignalsTest(number=1, string="One", lists=['1', '2']) pickle_doc = PickleSignalsTest(
number=1, string="One", lists=['1', '2'])
pickle_doc.embedded = PickleEmbedded() pickle_doc.embedded = PickleEmbedded()
pickle_doc.save() pickle_doc.save()
pickle_doc.delete() pickle_doc.delete()
@@ -2055,9 +2266,15 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(AuthorBooks._get_db(), get_db("testdb-3")) self.assertEqual(AuthorBooks._get_db(), get_db("testdb-3"))
# Collections # Collections
self.assertEqual(User._get_collection(), get_db("testdb-1")[User._get_collection_name()]) self.assertEqual(
self.assertEqual(Book._get_collection(), get_db("testdb-2")[Book._get_collection_name()]) User._get_collection(),
self.assertEqual(AuthorBooks._get_collection(), get_db("testdb-3")[AuthorBooks._get_collection_name()]) get_db("testdb-1")[User._get_collection_name()])
self.assertEqual(
Book._get_collection(),
get_db("testdb-2")[Book._get_collection_name()])
self.assertEqual(
AuthorBooks._get_collection(),
get_db("testdb-3")[AuthorBooks._get_collection_name()])
def test_db_alias_overrides(self): def test_db_alias_overrides(self):
"""db_alias can be overriden """db_alias can be overriden
@@ -2226,30 +2443,6 @@ class InstanceTest(unittest.TestCase):
group = Group.objects.first() group = Group.objects.first()
self.assertEqual("hello - default", group.name) self.assertEqual("hello - default", group.name)
def test_no_overwritting_no_data_loss(self):
class User(Document):
username = StringField(primary_key=True)
name = StringField()
@property
def foo(self):
return True
User.drop_collection()
user = User(username="Ross", foo="bar")
self.assertTrue(user.foo)
User._get_collection().save({"_id": "Ross", "foo": "Bar",
"data": [1, 2, 3]})
user = User.objects.first()
self.assertEqual("Ross", user.username)
self.assertEqual(True, user.foo)
self.assertEqual("Bar", user._data["foo"])
self.assertEqual([1, 2, 3], user._data["data"])
def test_spaces_in_keys(self): def test_spaces_in_keys(self):
class Embedded(DynamicEmbeddedDocument): class Embedded(DynamicEmbeddedDocument):
@@ -2281,6 +2474,8 @@ class InstanceTest(unittest.TestCase):
log.machine = "Localhost" log.machine = "Localhost"
log.save() log.save()
self.assertTrue(log.id is not None)
log.log = "Saving" log.log = "Saving"
log.save() log.save()
@@ -2304,6 +2499,8 @@ class InstanceTest(unittest.TestCase):
log.machine = "Localhost" log.machine = "Localhost"
log.save() log.save()
self.assertTrue(log.id is not None)
log.log = "Saving" log.log = "Saving"
log.save() log.save()
@@ -2321,6 +2518,10 @@ class InstanceTest(unittest.TestCase):
doc_name = StringField() doc_name = StringField()
doc = EmbeddedDocumentField(Embedded) doc = EmbeddedDocumentField(Embedded)
def __eq__(self, other):
return (self.doc_name == other.doc_name and
self.doc == other.doc)
classic_doc = Doc(doc_name="my doc", doc=Embedded(name="embedded doc")) classic_doc = Doc(doc_name="my doc", doc=Embedded(name="embedded doc"))
dict_doc = Doc(**{"doc_name": "my doc", dict_doc = Doc(**{"doc_name": "my doc",
"doc": {"name": "embedded doc"}}) "doc": {"name": "embedded doc"}})
@@ -2337,6 +2538,10 @@ class InstanceTest(unittest.TestCase):
doc_name = StringField() doc_name = StringField()
docs = ListField(EmbeddedDocumentField(Embedded)) docs = ListField(EmbeddedDocumentField(Embedded))
def __eq__(self, other):
return (self.doc_name == other.doc_name and
self.docs == other.docs)
classic_doc = Doc(doc_name="my doc", docs=[ classic_doc = Doc(doc_name="my doc", docs=[
Embedded(name="embedded doc1"), Embedded(name="embedded doc1"),
Embedded(name="embedded doc2")]) Embedded(name="embedded doc2")])
@@ -2411,7 +2616,7 @@ class InstanceTest(unittest.TestCase):
for parameter_name, parameter in self.parameters.iteritems(): for parameter_name, parameter in self.parameters.iteritems():
parameter.expand() parameter.expand()
class System(Document): class NodesSystem(Document):
name = StringField(required=True) name = StringField(required=True)
nodes = MapField(ReferenceField(Node, dbref=False)) nodes = MapField(ReferenceField(Node, dbref=False))
@@ -2419,19 +2624,21 @@ class InstanceTest(unittest.TestCase):
for node_name, node in self.nodes.iteritems(): for node_name, node in self.nodes.iteritems():
node.expand() node.expand()
node.save(*args, **kwargs) node.save(*args, **kwargs)
super(System, self).save(*args, **kwargs) super(NodesSystem, self).save(*args, **kwargs)
System.drop_collection() NodesSystem.drop_collection()
Node.drop_collection() Node.drop_collection()
system = System(name="system") system = NodesSystem(name="system")
system.nodes["node"] = Node() system.nodes["node"] = Node()
system.save() system.save()
system.nodes["node"].parameters["param"] = Parameter() system.nodes["node"].parameters["param"] = Parameter()
system.save() system.save()
system = System.objects.first() system = NodesSystem.objects.first()
self.assertEqual("UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value) self.assertEqual(
"UNDEFINED",
system.nodes["node"].parameters["param"].macros["test"].value)
def test_embedded_document_equality(self): def test_embedded_document_equality(self):
@@ -2452,5 +2659,145 @@ class InstanceTest(unittest.TestCase):
f1.ref # Dereferences lazily f1.ref # Dereferences lazily
self.assertEqual(f1, f2) self.assertEqual(f1, f2)
def test_dbref_equality(self):
class Test2(Document):
name = StringField()
class Test3(Document):
name = StringField()
class Test(Document):
name = StringField()
test2 = ReferenceField('Test2')
test3 = ReferenceField('Test3')
Test.drop_collection()
Test2.drop_collection()
Test3.drop_collection()
t2 = Test2(name='a')
t2.save()
t3 = Test3(name='x')
t3.id = t2.id
t3.save()
t = Test(name='b', test2=t2, test3=t3)
f = Test._from_son(t.to_mongo())
dbref2 = f._data['test2']
obj2 = f.test2
self.assertTrue(isinstance(dbref2, DBRef))
self.assertTrue(isinstance(obj2, Test2))
self.assertTrue(obj2.id == dbref2.id)
self.assertTrue(obj2 == dbref2)
self.assertTrue(dbref2 == obj2)
dbref3 = f._data['test3']
obj3 = f.test3
self.assertTrue(isinstance(dbref3, DBRef))
self.assertTrue(isinstance(obj3, Test3))
self.assertTrue(obj3.id == dbref3.id)
self.assertTrue(obj3 == dbref3)
self.assertTrue(dbref3 == obj3)
self.assertTrue(obj2.id == obj3.id)
self.assertTrue(dbref2.id == dbref3.id)
self.assertFalse(dbref2 == dbref3)
self.assertFalse(dbref3 == dbref2)
self.assertTrue(dbref2 != dbref3)
self.assertTrue(dbref3 != dbref2)
self.assertFalse(obj2 == dbref3)
self.assertFalse(dbref3 == obj2)
self.assertTrue(obj2 != dbref3)
self.assertTrue(dbref3 != obj2)
self.assertFalse(obj3 == dbref2)
self.assertFalse(dbref2 == obj3)
self.assertTrue(obj3 != dbref2)
self.assertTrue(dbref2 != obj3)
def test_default_values(self):
class Person(Document):
created_on = DateTimeField(default=lambda: datetime.utcnow())
name = StringField()
p = Person(name='alon')
p.save()
orig_created_on = Person.objects().only('created_on')[0].created_on
p2 = Person.objects().only('name')[0]
p2.name = 'alon2'
p2.save()
p3 = Person.objects().only('created_on')[0]
self.assertEquals(orig_created_on, p3.created_on)
class Person(Document):
created_on = DateTimeField(default=lambda: datetime.utcnow())
name = StringField()
height = IntField(default=189)
p4 = Person.objects()[0]
p4.save()
self.assertEquals(p4.height, 189)
self.assertEquals(Person.objects(height=189).count(), 1)
def test_from_son(self):
# 771
class MyPerson(self.Person):
meta = dict(shard_key=["id"])
p = MyPerson.from_json('{"name": "name", "age": 27}', created=True)
self.assertEquals(p.id, None)
p.id = "12345" # in case it is not working: "OperationError: Shard Keys are immutable..." will be raised here
p = MyPerson._from_son({"name": "name", "age": 27}, created=True)
self.assertEquals(p.id, None)
p.id = "12345" # in case it is not working: "OperationError: Shard Keys are immutable..." will be raised here
def test_null_field(self):
# 734
class User(Document):
name = StringField()
height = IntField(default=184, null=True)
str_fld = StringField(null=True)
int_fld = IntField(null=True)
flt_fld = FloatField(null=True)
dt_fld = DateTimeField(null=True)
cdt_fld = ComplexDateTimeField(null=True)
User.objects.delete()
u = User(name='user')
u.save()
u_from_db = User.objects.get(name='user')
u_from_db.height = None
u_from_db.save()
self.assertEquals(u_from_db.height, None)
# 864
self.assertEqual(u_from_db.str_fld, None)
self.assertEqual(u_from_db.int_fld, None)
self.assertEqual(u_from_db.flt_fld, None)
self.assertEqual(u_from_db.dt_fld, None)
self.assertEqual(u_from_db.cdt_fld, None)
# 735
User.objects.delete()
u = User(name='user')
u.save()
User.objects(name='user').update_one(set__height=None, upsert=True)
u_from_db = User.objects.get(name='user')
self.assertEquals(u_from_db.height, None)
def test_not_saved_eq(self):
"""Ensure we can compare documents not saved.
"""
class Person(Document):
pass
p = Person()
p1 = Person()
self.assertNotEqual(p, p1)
self.assertEqual(p, p)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -20,6 +20,28 @@ class TestJson(unittest.TestCase):
def setUp(self): def setUp(self):
connect(db='mongoenginetest') connect(db='mongoenginetest')
def test_json_names(self):
"""
Going to test reported issue:
https://github.com/MongoEngine/mongoengine/issues/654
where the reporter asks for the availability to perform
a to_json with the original class names and not the abreviated
mongodb document keys
"""
class Embedded(EmbeddedDocument):
string = StringField(db_field='s')
class Doc(Document):
string = StringField(db_field='s')
embedded = EmbeddedDocumentField(Embedded, db_field='e')
doc = Doc( string="Hello", embedded=Embedded(string="Inner Hello"))
doc_json = doc.to_json(sort_keys=True, use_db_field=False,separators=(',', ':'))
expected_json = """{"embedded":{"string":"Inner Hello"},"string":"Hello"}"""
self.assertEqual( doc_json, expected_json)
def test_json_simple(self): def test_json_simple(self):
class Embedded(EmbeddedDocument): class Embedded(EmbeddedDocument):
@@ -29,6 +51,10 @@ class TestJson(unittest.TestCase):
string = StringField() string = StringField()
embedded_field = EmbeddedDocumentField(Embedded) embedded_field = EmbeddedDocumentField(Embedded)
def __eq__(self, other):
return (self.string == other.string and
self.embedded_field == other.embedded_field)
doc = Doc(string="Hi", embedded_field=Embedded(string="Hi")) doc = Doc(string="Hi", embedded_field=Embedded(string="Hi"))
doc_json = doc.to_json(sort_keys=True, separators=(',', ':')) doc_json = doc.to_json(sort_keys=True, separators=(',', ':'))
@@ -77,6 +103,10 @@ class TestJson(unittest.TestCase):
generic_embedded_document_field = GenericEmbeddedDocumentField( generic_embedded_document_field = GenericEmbeddedDocumentField(
default=lambda: EmbeddedDoc()) default=lambda: EmbeddedDoc())
def __eq__(self, other):
import json
return json.loads(self.to_json()) == json.loads(other.to_json())
doc = Doc() doc = Doc()
self.assertEqual(doc, Doc.from_json(doc.to_json())) self.assertEqual(doc, Doc.from_json(doc.to_json()))

View File

@@ -141,6 +141,30 @@ class ValidatorErrorTest(unittest.TestCase):
self.assertEqual(e.to_dict(), { self.assertEqual(e.to_dict(), {
"e": {'val': 'OK could not be converted to int'}}) "e": {'val': 'OK could not be converted to int'}})
def test_embedded_weakref(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
e = EmbeddedDocumentField(SubDoc, db_field='eb')
Doc.drop_collection()
d1 = Doc()
d2 = Doc()
s = SubDoc()
self.assertRaises(ValidationError, lambda: s.validate())
d1.e = s
d2.e = s
del d1
self.assertRaises(ValidationError, lambda: d2.validate())
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

File diff suppressed because it is too large Load Diff

View File

@@ -114,6 +114,42 @@ class FileTest(unittest.TestCase):
# Ensure deleted file returns None # Ensure deleted file returns None
self.assertTrue(result.the_file.read() == None) self.assertTrue(result.the_file.read() == None)
def test_file_fields_stream_after_none(self):
"""Ensure that a file field can be written to after it has been saved as
None
"""
class StreamFile(Document):
the_file = FileField()
StreamFile.drop_collection()
text = b('Hello, World!')
more_text = b('Foo Bar')
content_type = 'text/plain'
streamfile = StreamFile()
streamfile.save()
streamfile.the_file.new_file()
streamfile.the_file.write(text)
streamfile.the_file.write(more_text)
streamfile.the_file.close()
streamfile.save()
result = StreamFile.objects.first()
self.assertTrue(streamfile == result)
self.assertEqual(result.the_file.read(), text + more_text)
#self.assertEqual(result.the_file.content_type, content_type)
result.the_file.seek(0)
self.assertEqual(result.the_file.tell(), 0)
self.assertEqual(result.the_file.read(len(text)), text)
self.assertEqual(result.the_file.tell(), len(text))
self.assertEqual(result.the_file.read(len(more_text)), more_text)
self.assertEqual(result.the_file.tell(), len(text + more_text))
result.the_file.delete()
# Ensure deleted file returns None
self.assertTrue(result.the_file.read() == None)
def test_file_fields_set(self): def test_file_fields_set(self):
class SetFile(Document): class SetFile(Document):
@@ -279,7 +315,7 @@ class FileTest(unittest.TestCase):
t.image.put(f) t.image.put(f)
self.fail("Should have raised an invalidation error") self.fail("Should have raised an invalidation error")
except ValidationError, e: except ValidationError, e:
self.assertEqual("%s" % e, "Invalid image: cannot identify image file") self.assertEqual("%s" % e, "Invalid image: cannot identify image file %s" % f)
t = TestImage() t = TestImage()
t.image.put(open(TEST_IMAGE_PATH, 'rb')) t.image.put(open(TEST_IMAGE_PATH, 'rb'))

View File

@@ -19,8 +19,8 @@ class GeoFieldTest(unittest.TestCase):
def _test_for_expected_error(self, Cls, loc, expected): def _test_for_expected_error(self, Cls, loc, expected):
try: try:
Cls(loc=loc).validate() Cls(loc=loc).validate()
self.fail() self.fail('Should not validate the location {0}'.format(loc))
except ValidationError, e: except ValidationError as e:
self.assertEqual(expected, e.to_dict()['loc']) self.assertEqual(expected, e.to_dict()['loc'])
def test_geopoint_validation(self): def test_geopoint_validation(self):
@@ -155,6 +155,117 @@ class GeoFieldTest(unittest.TestCase):
Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate() Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate()
def test_multipoint_validation(self):
class Location(Document):
loc = MultiPointField()
invalid_coords = {"x": 1, "y": 2}
expected = 'MultiPointField can only accept a valid GeoJson dictionary or lists of (x, y)'
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
expected = 'MultiPointField type must be "MultiPoint"'
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MultiPoint", "coordinates": [[1, 2, 3]]}
expected = "Value ([1, 2, 3]) must be a two-dimensional point"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[]]
expected = "Invalid MultiPoint must contain at least one valid point"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[1]], [[1, 2, 3]]]
for coord in invalid_coords:
expected = "Value (%s) must be a two-dimensional point" % repr(coord[0])
self._test_for_expected_error(Location, coord, expected)
invalid_coords = [[[{}, {}]], [("a", "b")]]
for coord in invalid_coords:
expected = "Both values (%s) in point must be float or int" % repr(coord[0])
self._test_for_expected_error(Location, coord, expected)
Location(loc=[[1, 2]]).validate()
Location(loc={
"type": "MultiPoint",
"coordinates": [
[1, 2],
[81.4471435546875, 23.61432859499169]
]}).validate()
def test_multilinestring_validation(self):
class Location(Document):
loc = MultiLineStringField()
invalid_coords = {"x": 1, "y": 2}
expected = 'MultiLineStringField can only accept a valid GeoJson dictionary or lists of (x, y)'
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
expected = 'MultiLineStringField type must be "MultiLineString"'
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MultiLineString", "coordinates": [[[1, 2, 3]]]}
expected = "Invalid MultiLineString:\nValue ([1, 2, 3]) must be a two-dimensional point"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [5, "a"]
expected = "Invalid MultiLineString must contain at least one valid linestring"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[1]]]
expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0])
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[1, 2, 3]]]
expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0])
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[{}, {}]]], [[("a", "b")]]]
for coord in invalid_coords:
expected = "Invalid MultiLineString:\nBoth values (%s) in point must be float or int" % repr(coord[0][0])
self._test_for_expected_error(Location, coord, expected)
Location(loc=[[[1, 2], [3, 4], [5, 6], [1,2]]]).validate()
def test_multipolygon_validation(self):
class Location(Document):
loc = MultiPolygonField()
invalid_coords = {"x": 1, "y": 2}
expected = 'MultiPolygonField can only accept a valid GeoJson dictionary or lists of (x, y)'
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
expected = 'MultiPolygonField type must be "MultiPolygon"'
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MultiPolygon", "coordinates": [[[[1, 2, 3]]]]}
expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[5, "a"]]]]
expected = "Invalid MultiPolygon:\nBoth values ([5, 'a']) in point must be float or int"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[]]]]
expected = "Invalid MultiPolygon must contain at least one valid Polygon"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[1, 2, 3]]]]
expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[{}, {}]]], [[("a", "b")]]]
expected = "Invalid MultiPolygon:\nBoth values ([{}, {}]) in point must be float or int, Both values (('a', 'b')) in point must be float or int"
self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[1, 2], [3, 4]]]]
expected = "Invalid MultiPolygon:\nLineStrings must start and end at the same point"
self._test_for_expected_error(Location, invalid_coords, expected)
Location(loc=[[[[1, 2], [3, 4], [5, 6], [1, 2]]]]).validate()
def test_indexes_geopoint(self): def test_indexes_geopoint(self):
"""Ensure that indexes are created automatically for GeoPointFields. """Ensure that indexes are created automatically for GeoPointFields.
""" """

View File

@@ -3,3 +3,4 @@ from field_list import *
from queryset import * from queryset import *
from visitor import * from visitor import *
from geo import * from geo import *
from modify import *

View File

@@ -5,6 +5,8 @@ import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from mongoengine import * from mongoengine import *
from nose.plugins.skip import SkipTest
__all__ = ("GeoQueriesTest",) __all__ = ("GeoQueriesTest",)
@@ -139,6 +141,7 @@ class GeoQueriesTest(unittest.TestCase):
def test_spherical_geospatial_operators(self): def test_spherical_geospatial_operators(self):
"""Ensure that spherical geospatial queries are working """Ensure that spherical geospatial queries are working
""" """
raise SkipTest("https://jira.mongodb.org/browse/SERVER-14039")
class Point(Document): class Point(Document):
location = GeoPointField() location = GeoPointField()

102
tests/queryset/modify.py Normal file
View File

@@ -0,0 +1,102 @@
import sys
sys.path[0:0] = [""]
import unittest
from mongoengine import connect, Document, IntField
__all__ = ("FindAndModifyTest",)
class Doc(Document):
id = IntField(primary_key=True)
value = IntField()
class FindAndModifyTest(unittest.TestCase):
def setUp(self):
connect(db="mongoenginetest")
Doc.drop_collection()
def assertDbEqual(self, docs):
self.assertEqual(list(Doc._collection.find().sort("id")), docs)
def test_modify(self):
Doc(id=0, value=0).save()
doc = Doc(id=1, value=1).save()
old_doc = Doc.objects(id=1).modify(set__value=-1)
self.assertEqual(old_doc.to_json(), doc.to_json())
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
def test_modify_with_new(self):
Doc(id=0, value=0).save()
doc = Doc(id=1, value=1).save()
new_doc = Doc.objects(id=1).modify(set__value=-1, new=True)
doc.value = -1
self.assertEqual(new_doc.to_json(), doc.to_json())
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
def test_modify_not_existing(self):
Doc(id=0, value=0).save()
self.assertEqual(Doc.objects(id=1).modify(set__value=-1), None)
self.assertDbEqual([{"_id": 0, "value": 0}])
def test_modify_with_upsert(self):
Doc(id=0, value=0).save()
old_doc = Doc.objects(id=1).modify(set__value=1, upsert=True)
self.assertEqual(old_doc, None)
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": 1}])
def test_modify_with_upsert_existing(self):
Doc(id=0, value=0).save()
doc = Doc(id=1, value=1).save()
old_doc = Doc.objects(id=1).modify(set__value=-1, upsert=True)
self.assertEqual(old_doc.to_json(), doc.to_json())
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
def test_modify_with_upsert_with_new(self):
Doc(id=0, value=0).save()
new_doc = Doc.objects(id=1).modify(upsert=True, new=True, set__value=1)
self.assertEqual(new_doc.to_mongo(), {"_id": 1, "value": 1})
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": 1}])
def test_modify_with_remove(self):
Doc(id=0, value=0).save()
doc = Doc(id=1, value=1).save()
old_doc = Doc.objects(id=1).modify(remove=True)
self.assertEqual(old_doc.to_json(), doc.to_json())
self.assertDbEqual([{"_id": 0, "value": 0}])
def test_find_and_modify_with_remove_not_existing(self):
Doc(id=0, value=0).save()
self.assertEqual(Doc.objects(id=1).modify(remove=True), None)
self.assertDbEqual([{"_id": 0, "value": 0}])
def test_modify_with_order_by(self):
Doc(id=0, value=3).save()
Doc(id=1, value=2).save()
Doc(id=2, value=1).save()
doc = Doc(id=3, value=0).save()
old_doc = Doc.objects().order_by("-id").modify(set__value=-1)
self.assertEqual(old_doc.to_json(), doc.to_json())
self.assertDbEqual([
{"_id": 0, "value": 3}, {"_id": 1, "value": 2},
{"_id": 2, "value": 1}, {"_id": 3, "value": -1}])
def test_modify_with_fields(self):
Doc(id=0, value=0).save()
Doc(id=1, value=1).save()
old_doc = Doc.objects(id=1).only("id").modify(set__value=-1)
self.assertEqual(old_doc.to_mongo(), {"_id": 1})
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
if __name__ == '__main__':
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@@ -197,5 +197,17 @@ class TransformTest(unittest.TestCase):
update = transform.update(Location, set__poly={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) update = transform.update(Location, set__poly={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]})
self.assertEqual(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}}) self.assertEqual(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}})
def test_type(self):
class Doc(Document):
df = DynamicField()
Doc(df=True).save()
Doc(df=7).save()
Doc(df="df").save()
self.assertEqual(Doc.objects(df__type=1).count(), 0) # double
self.assertEqual(Doc.objects(df__type=8).count(), 1) # bool
self.assertEqual(Doc.objects(df__type=2).count(), 1) # str
self.assertEqual(Doc.objects(df__type=16).count(), 1) # int
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -1,6 +1,11 @@
import sys import sys
sys.path[0:0] = [""] sys.path[0:0] = [""]
import unittest
try:
import unittest2 as unittest
except ImportError:
import unittest
import datetime import datetime
import pymongo import pymongo
@@ -34,6 +39,17 @@ class ConnectionTest(unittest.TestCase):
conn = get_connection('testdb') conn = get_connection('testdb')
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
def test_sharing_connections(self):
"""Ensure that connections are shared when the connection settings are exactly the same
"""
connect('mongoenginetest', alias='testdb1')
expected_connection = get_connection('testdb1')
connect('mongoenginetest', alias='testdb2')
actual_connection = get_connection('testdb2')
self.assertEqual(expected_connection, actual_connection)
def test_connect_uri(self): def test_connect_uri(self):
"""Ensure that the connect() method works properly with uri's """Ensure that the connect() method works properly with uri's
""" """
@@ -131,6 +147,18 @@ class ConnectionTest(unittest.TestCase):
date_doc = DateDoc.objects.first() date_doc = DateDoc.objects.first()
self.assertEqual(d, date_doc.the_date) self.assertEqual(d, date_doc.the_date)
def test_multiple_connection_settings(self):
connect('mongoenginetest', alias='t1', host="localhost")
connect('mongoenginetest2', alias='t2', host="127.0.0.1")
mongo_connections = mongoengine.connection._connections
self.assertEqual(len(mongo_connections.items()), 2)
self.assertTrue('t1' in mongo_connections.keys())
self.assertTrue('t2' in mongo_connections.keys())
self.assertEqual(mongo_connections['t1'].host, 'localhost')
self.assertEqual(mongo_connections['t2'].host, '127.0.0.1')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -0,0 +1,107 @@
import unittest
from mongoengine.base.datastructures import StrictDict, SemiStrictDict
class TestStrictDict(unittest.TestCase):
def strict_dict_class(self, *args, **kwargs):
return StrictDict.create(*args, **kwargs)
def setUp(self):
self.dtype = self.strict_dict_class(("a", "b", "c"))
def test_init(self):
d = self.dtype(a=1, b=1, c=1)
self.assertEqual((d.a, d.b, d.c), (1, 1, 1))
def test_init_fails_on_nonexisting_attrs(self):
self.assertRaises(AttributeError, lambda: self.dtype(a=1, b=2, d=3))
def test_eq(self):
d = self.dtype(a=1, b=1, c=1)
dd = self.dtype(a=1, b=1, c=1)
e = self.dtype(a=1, b=1, c=3)
f = self.dtype(a=1, b=1)
g = self.strict_dict_class(("a", "b", "c", "d"))(a=1, b=1, c=1, d=1)
h = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=1)
i = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=2)
self.assertEqual(d, dd)
self.assertNotEqual(d, e)
self.assertNotEqual(d, f)
self.assertNotEqual(d, g)
self.assertNotEqual(f, d)
self.assertEqual(d, h)
self.assertNotEqual(d, i)
def test_setattr_getattr(self):
d = self.dtype()
d.a = 1
self.assertEqual(d.a, 1)
self.assertRaises(AttributeError, lambda: d.b)
def test_setattr_raises_on_nonexisting_attr(self):
d = self.dtype()
def _f():
d.x=1
self.assertRaises(AttributeError, _f)
def test_setattr_getattr_special(self):
d = self.strict_dict_class(["items"])
d.items = 1
self.assertEqual(d.items, 1)
def test_get(self):
d = self.dtype(a=1)
self.assertEqual(d.get('a'), 1)
self.assertEqual(d.get('b', 'bla'), 'bla')
def test_items(self):
d = self.dtype(a=1)
self.assertEqual(d.items(), [('a', 1)])
d = self.dtype(a=1, b=2)
self.assertEqual(d.items(), [('a', 1), ('b', 2)])
def test_mappings_protocol(self):
d = self.dtype(a=1, b=2)
assert dict(d) == {'a': 1, 'b': 2}
assert dict(**d) == {'a': 1, 'b': 2}
class TestSemiSrictDict(TestStrictDict):
def strict_dict_class(self, *args, **kwargs):
return SemiStrictDict.create(*args, **kwargs)
def test_init_fails_on_nonexisting_attrs(self):
# disable irrelevant test
pass
def test_setattr_raises_on_nonexisting_attr(self):
# disable irrelevant test
pass
def test_setattr_getattr_nonexisting_attr_succeeds(self):
d = self.dtype()
d.x = 1
self.assertEqual(d.x, 1)
def test_init_succeeds_with_nonexisting_attrs(self):
d = self.dtype(a=1, b=1, c=1, x=2)
self.assertEqual((d.a, d.b, d.c, d.x), (1, 1, 1, 2))
def test_iter_with_nonexisting_attrs(self):
d = self.dtype(a=1, b=1, c=1, x=2)
self.assertEqual(list(d), ['a', 'b', 'c', 'x'])
def test_iteritems_with_nonexisting_attrs(self):
d = self.dtype(a=1, b=1, c=1, x=2)
self.assertEqual(list(d.iteritems()), [('a', 1), ('b', 1), ('c', 1), ('x', 2)])
def tets_cmp_with_strict_dicts(self):
d = self.dtype(a=1, b=1, c=1)
dd = StrictDict.create(("a", "b", "c"))(a=1, b=1, c=1)
self.assertEqual(d, dd)
def test_cmp_with_strict_dict_with_nonexisting_attrs(self):
d = self.dtype(a=1, b=1, c=1, x=2)
dd = StrictDict.create(("a", "b", "c", "x"))(a=1, b=1, c=1, x=2)
self.assertEqual(d, dd)
if __name__ == '__main__':
unittest.main()

View File

@@ -291,9 +291,37 @@ class FieldTest(unittest.TestCase):
self.assertEqual(employee.friends, friends) self.assertEqual(employee.friends, friends)
self.assertEqual(q, 2) self.assertEqual(q, 2)
def test_list_of_lists_of_references(self):
class User(Document):
name = StringField()
class Post(Document):
user_lists = ListField(ListField(ReferenceField(User)))
class SimpleList(Document):
users = ListField(ReferenceField(User))
User.drop_collection()
Post.drop_collection()
u1 = User.objects.create(name='u1')
u2 = User.objects.create(name='u2')
u3 = User.objects.create(name='u3')
SimpleList.objects.create(users=[u1, u2, u3])
self.assertEqual(SimpleList.objects.all()[0].users, [u1, u2, u3])
Post.objects.create(user_lists=[[u1, u2], [u3]])
self.assertEqual(Post.objects.all()[0].user_lists, [[u1, u2], [u3]])
def test_circular_reference(self): def test_circular_reference(self):
"""Ensure you can handle circular references """Ensure you can handle circular references
""" """
class Relation(EmbeddedDocument):
name = StringField()
person = ReferenceField('Person')
class Person(Document): class Person(Document):
name = StringField() name = StringField()
relations = ListField(EmbeddedDocumentField('Relation')) relations = ListField(EmbeddedDocumentField('Relation'))
@@ -301,10 +329,6 @@ class FieldTest(unittest.TestCase):
def __repr__(self): def __repr__(self):
return "<Person: %s>" % self.name return "<Person: %s>" % self.name
class Relation(EmbeddedDocument):
name = StringField()
person = ReferenceField('Person')
Person.drop_collection() Person.drop_collection()
mother = Person(name="Mother") mother = Person(name="Mother")
daughter = Person(name="Daughter") daughter = Person(name="Daughter")
@@ -923,6 +947,8 @@ class FieldTest(unittest.TestCase):
class Asset(Document): class Asset(Document):
name = StringField(max_length=250, required=True) name = StringField(max_length=250, required=True)
path = StringField()
title = StringField()
parent = GenericReferenceField(default=None) parent = GenericReferenceField(default=None)
parents = ListField(GenericReferenceField()) parents = ListField(GenericReferenceField())
children = ListField(GenericReferenceField()) children = ListField(GenericReferenceField())
@@ -1195,6 +1221,31 @@ class FieldTest(unittest.TestCase):
page = Page.objects.first() page = Page.objects.first()
self.assertEqual(page.tags[0], page.posts[0].tags[0]) self.assertEqual(page.tags[0], page.posts[0].tags[0])
def test_select_related_follows_embedded_referencefields(self):
class Song(Document):
title = StringField()
class PlaylistItem(EmbeddedDocument):
song = ReferenceField("Song")
class Playlist(Document):
items = ListField(EmbeddedDocumentField("PlaylistItem"))
Playlist.drop_collection()
Song.drop_collection()
songs = [Song.objects.create(title="song %d" % i) for i in range(3)]
items = [PlaylistItem(song=song) for song in songs]
playlist = Playlist.objects.create(items=items)
with query_counter() as q:
self.assertEqual(q, 0)
playlist = Playlist.objects.first().select_related()
songs = [item.song for item in playlist.items]
self.assertEqual(q, 2)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -2,11 +2,11 @@ import sys
sys.path[0:0] = [""] sys.path[0:0] = [""]
import unittest import unittest
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
from mongoengine import * from mongoengine import *
from mongoengine.django.shortcuts import get_document_or_404 from mongoengine.django.shortcuts import get_document_or_404
import django
from django.http import Http404 from django.http import Http404
from django.template import Context, Template from django.template import Context, Template
from django.conf import settings from django.conf import settings
@@ -19,6 +19,13 @@ settings.configure(
AUTHENTICATION_BACKENDS = ('mongoengine.django.auth.MongoEngineBackend',) AUTHENTICATION_BACKENDS = ('mongoengine.django.auth.MongoEngineBackend',)
) )
try:
# For Django >= 1.7
if hasattr(django, 'setup'):
django.setup()
except RuntimeError:
pass
try: try:
from django.contrib.auth import authenticate, get_user_model from django.contrib.auth import authenticate, get_user_model
from mongoengine.django.auth import User from mongoengine.django.auth import User
@@ -30,8 +37,8 @@ try:
DJ15 = True DJ15 = True
except Exception: except Exception:
DJ15 = False DJ15 = False
from django.contrib.sessions.tests import SessionTestsMixin
from mongoengine.django.sessions import SessionStore, MongoSession from mongoengine.django.sessions import SessionStore, MongoSession
from mongoengine.django.tests import MongoTestCase
from datetime import tzinfo, timedelta from datetime import tzinfo, timedelta
ZERO = timedelta(0) ZERO = timedelta(0)
@@ -165,8 +172,13 @@ class QuerySetTest(unittest.TestCase):
"""Ensure that a queryset and filters work as expected """Ensure that a queryset and filters work as expected
""" """
class LimitCountQuerySet(QuerySet):
def count(self, with_limit_and_skip=True):
return super(LimitCountQuerySet, self).count(with_limit_and_skip)
class Note(Document): class Note(Document):
text = StringField() meta = dict(queryset_class=LimitCountQuerySet)
name = StringField()
Note.drop_collection() Note.drop_collection()
@@ -216,13 +228,13 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(t.render(c), "10") self.assertEqual(t.render(c), "10")
class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase): class _BaseMongoDBSessionTest(unittest.TestCase):
backend = SessionStore backend = SessionStore
def setUp(self): def setUp(self):
connect(db='mongoenginetest') connect(db='mongoenginetest')
MongoSession.drop_collection() MongoSession.drop_collection()
super(MongoDBSessionTest, self).setUp() super(_BaseMongoDBSessionTest, self).setUp()
def assertIn(self, first, second, msg=None): def assertIn(self, first, second, msg=None):
self.assertTrue(first in second, msg) self.assertTrue(first in second, msg)
@@ -249,6 +261,21 @@ class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
self.assertTrue('test_expire' in session, 'Session has expired before it is expected') self.assertTrue('test_expire' in session, 'Session has expired before it is expected')
try:
# SessionTestsMixin isn't available for import on django > 1.8a1
from django.contrib.sessions.tests import SessionTestsMixin
class _MongoDBSessionTest(SessionTestsMixin):
pass
class MongoDBSessionTest(_BaseMongoDBSessionTest):
pass
except ImportError:
class MongoDBSessionTest(_BaseMongoDBSessionTest):
pass
class MongoAuthTest(unittest.TestCase): class MongoAuthTest(unittest.TestCase):
user_data = { user_data = {
'username': 'user', 'username': 'user',
@@ -293,5 +320,11 @@ class MongoAuthTest(unittest.TestCase):
db_user = User.objects.get(username='user') db_user = User.objects.get(username='user')
self.assertEqual(user.id, db_user.id) self.assertEqual(user.id, db_user.id)
class MongoTestCaseTest(MongoTestCase):
def test_mongo_test_case(self):
self.db.dummy_collection.insert({'collection': 'will be dropped'})
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -37,7 +37,8 @@ class SignalTests(unittest.TestCase):
@classmethod @classmethod
def post_init(cls, sender, document, **kwargs): def post_init(cls, sender, document, **kwargs):
signal_output.append('post_init signal, %s' % document) signal_output.append('post_init signal, %s, document._created = %s' % (document, document._created))
@classmethod @classmethod
def pre_save(cls, sender, document, **kwargs): def pre_save(cls, sender, document, **kwargs):
@@ -54,7 +55,9 @@ class SignalTests(unittest.TestCase):
@classmethod @classmethod
def post_save(cls, sender, document, **kwargs): def post_save(cls, sender, document, **kwargs):
dirty_keys = document._delta()[0].keys() + document._delta()[1].keys()
signal_output.append('post_save signal, %s' % document) signal_output.append('post_save signal, %s' % document)
signal_output.append('post_save dirty keys, %s' % dirty_keys)
if 'created' in kwargs: if 'created' in kwargs:
if kwargs['created']: if kwargs['created']:
signal_output.append('Is created') signal_output.append('Is created')
@@ -191,10 +194,16 @@ class SignalTests(unittest.TestCase):
a1 = self.Author(name='Bill Shakespeare') a1 = self.Author(name='Bill Shakespeare')
self.Author.objects.insert([a1], load_bulk=False) self.Author.objects.insert([a1], load_bulk=False)
def load_existing_author():
a = self.Author(name='Bill Shakespeare')
a.save()
self.get_signal_output(lambda: None) # eliminate signal output
a1 = self.Author.objects(name='Bill Shakespeare')[0]
self.assertEqual(self.get_signal_output(create_author), [ self.assertEqual(self.get_signal_output(create_author), [
"pre_init signal, Author", "pre_init signal, Author",
"{'name': 'Bill Shakespeare'}", "{'name': 'Bill Shakespeare'}",
"post_init signal, Bill Shakespeare", "post_init signal, Bill Shakespeare, document._created = True",
]) ])
a1 = self.Author(name='Bill Shakespeare') a1 = self.Author(name='Bill Shakespeare')
@@ -203,6 +212,7 @@ class SignalTests(unittest.TestCase):
"pre_save_post_validation signal, Bill Shakespeare", "pre_save_post_validation signal, Bill Shakespeare",
"Is created", "Is created",
"post_save signal, Bill Shakespeare", "post_save signal, Bill Shakespeare",
"post_save dirty keys, ['name']",
"Is created" "Is created"
]) ])
@@ -213,6 +223,7 @@ class SignalTests(unittest.TestCase):
"pre_save_post_validation signal, William Shakespeare", "pre_save_post_validation signal, William Shakespeare",
"Is updated", "Is updated",
"post_save signal, William Shakespeare", "post_save signal, William Shakespeare",
"post_save dirty keys, ['name']",
"Is updated" "Is updated"
]) ])
@@ -221,12 +232,22 @@ class SignalTests(unittest.TestCase):
'post_delete signal, William Shakespeare', 'post_delete signal, William Shakespeare',
]) ])
signal_output = self.get_signal_output(load_existing_author)
# test signal_output lines separately, because of random ObjectID after object load
self.assertEqual(signal_output[0],
"pre_init signal, Author",
)
self.assertEqual(signal_output[2],
"post_init signal, Bill Shakespeare, document._created = False",
)
signal_output = self.get_signal_output(bulk_create_author_with_load) signal_output = self.get_signal_output(bulk_create_author_with_load)
# The output of this signal is not entirely deterministic. The reloaded # 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 # object will have an object ID. Hence, we only check part of the output
self.assertEqual(signal_output[3], self.assertEqual(signal_output[3], "pre_bulk_insert signal, [<Author: Bill Shakespeare>]"
"pre_bulk_insert signal, [<Author: Bill Shakespeare>]") )
self.assertEqual(signal_output[-2:], self.assertEqual(signal_output[-2:],
["post_bulk_insert signal, [<Author: Bill Shakespeare>]", ["post_bulk_insert signal, [<Author: Bill Shakespeare>]",
"Is loaded",]) "Is loaded",])
@@ -234,7 +255,7 @@ class SignalTests(unittest.TestCase):
self.assertEqual(self.get_signal_output(bulk_create_author_without_load), [ self.assertEqual(self.get_signal_output(bulk_create_author_without_load), [
"pre_init signal, Author", "pre_init signal, Author",
"{'name': 'Bill Shakespeare'}", "{'name': 'Bill Shakespeare'}",
"post_init signal, Bill Shakespeare", "post_init signal, Bill Shakespeare, document._created = True",
"pre_bulk_insert signal, [<Author: Bill Shakespeare>]", "pre_bulk_insert signal, [<Author: Bill Shakespeare>]",
"post_bulk_insert signal, [<Author: Bill Shakespeare>]", "post_bulk_insert signal, [<Author: Bill Shakespeare>]",
"Not loaded", "Not loaded",