Compare commits
159 Commits
topic/land
...
v0.9.0
Author | SHA1 | Date | |
---|---|---|---|
|
cbd2a44350 | ||
|
c888e461ba | ||
|
d135522087 | ||
|
ce2b148dd2 | ||
|
2d075c4dd6 | ||
|
bcd1841f71 | ||
|
029cf4ad1f | ||
|
ed7fc86d69 | ||
|
82a9e43b6f | ||
|
9ae2c731ed | ||
|
7d1ba466b4 | ||
|
4f1d8678ea | ||
|
4bd72ebc63 | ||
|
e5986e0ae2 | ||
|
fae39e4bc9 | ||
|
dbe8357dd5 | ||
|
3234f0bdd7 | ||
|
47a4d58009 | ||
|
4ae60da58d | ||
|
47f995bda3 | ||
|
42721628eb | ||
|
f42ab957d4 | ||
|
ce9d0d7e82 | ||
|
baf79dda21 | ||
|
b71a9bc097 | ||
|
129632cd6b | ||
|
aca8899c4d | ||
|
5c3d91e65e | ||
|
0205d827f1 | ||
|
225c31d583 | ||
|
b18d87ddba | ||
|
25298c72bb | ||
|
3df3d27533 | ||
|
cbb0b57018 | ||
|
65f205bca8 | ||
|
1cc7f80109 | ||
|
213a0a18a5 | ||
|
1a24d599b3 | ||
|
d80be60e2b | ||
|
0ffe79d76c | ||
|
db36d0a375 | ||
|
ff659a0be3 | ||
|
8485b12102 | ||
|
d889cc3c5a | ||
|
7bb65fca4e | ||
|
8aaa5951ca | ||
|
d58f3b7520 | ||
|
e5a636a159 | ||
|
51f314e907 | ||
|
531fa30b69 | ||
|
2b3bb81fae | ||
|
80f80cd31f | ||
|
79705fbf11 | ||
|
191a4e569e | ||
|
1cac35be03 | ||
|
6d48100f44 | ||
|
4627af3e90 | ||
|
913952ffe1 | ||
|
67bf6afc89 | ||
|
06064decd2 | ||
|
4cca9f17df | ||
|
74a89223c0 | ||
|
2954017836 | ||
|
a03262fc01 | ||
|
d65ce6fc2c | ||
|
d27e1eee25 | ||
|
b1f00bb708 | ||
|
e0f1e79e6a | ||
|
d70b7d41e8 | ||
|
43af9f3fad | ||
|
bc53dd6830 | ||
|
263616ef01 | ||
|
285da0542e | ||
|
17f7e2f892 | ||
|
a29d8f1d68 | ||
|
8965172603 | ||
|
03c2967337 | ||
|
5b154a0da4 | ||
|
b2c8c326d7 | ||
|
96aedaa91f | ||
|
a22ad1ec32 | ||
|
a4244defb5 | ||
|
57328e55f3 | ||
|
87c32aeb40 | ||
|
2e01e0c30e | ||
|
a12b2de74a | ||
|
6b01d8f99b | ||
|
eac4f6062e | ||
|
5583cf0a5f | ||
|
57d772fa23 | ||
|
1bdc3988a9 | ||
|
2af55baa9a | ||
|
0452eec11d | ||
|
c4f7db6c04 | ||
|
3569529a84 | ||
|
70942ac0f6 | ||
|
dc02e39918 | ||
|
73d6bc35ec | ||
|
b1d558d700 | ||
|
897480265f | ||
|
73724f5a33 | ||
|
bdbd495a9e | ||
|
1fcf009804 | ||
|
914c5752a5 | ||
|
201b12a886 | ||
|
c5f23ad93d | ||
|
28d62009a7 | ||
|
1a5a436f82 | ||
|
1275ac0569 | ||
|
5112fb777e | ||
|
f571a944c9 | ||
|
bc9aff8c60 | ||
|
c4c7ab7888 | ||
|
d9819a990c | ||
|
aea400e26a | ||
|
eb4e7735c1 | ||
|
4b498ae8cd | ||
|
158e2a4ca9 | ||
|
b011d48d82 | ||
|
8ac3e725f8 | ||
|
9a4aef0358 | ||
|
7d3146234a | ||
|
5d2ca6493d | ||
|
4752f9aa37 | ||
|
025d3a03d6 | ||
|
aec06183e7 | ||
|
aa28abd517 | ||
|
7430b31697 | ||
|
759f72169a | ||
|
1f7135be61 | ||
|
6942f9c1cf | ||
|
d9da75d1c0 | ||
|
7ab7372be4 | ||
|
3503c98857 | ||
|
708c3f1e2a | ||
|
6f645e8619 | ||
|
bce7ca7ac4 | ||
|
350465c25d | ||
|
5b9c70ae22 | ||
|
9b30afeca9 | ||
|
c1b202c119 | ||
|
41cfe5d2ca | ||
|
05339e184f | ||
|
447127d956 | ||
|
394334fbea | ||
|
9f8cd33d43 | ||
|
f066e28c35 | ||
|
b349a449bb | ||
|
1c5898d396 | ||
|
6802967863 | ||
|
0462f18680 | ||
|
af6699098f | ||
|
6b7e7dc124 | ||
|
6bae4c6a66 | ||
|
46da918dbe | ||
|
bb7e5f17b5 | ||
|
b9d03114c2 | ||
|
436b1ce176 | ||
|
85336f9777 |
102
.travis.yml
102
.travis.yml
@@ -1,50 +1,72 @@
|
||||
# http://travis-ci.org/#!/MongoEngine/mongoengine
|
||||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "pypy"
|
||||
- "pypy3"
|
||||
- '2.6'
|
||||
- '2.7'
|
||||
- '3.2'
|
||||
- '3.3'
|
||||
- '3.4'
|
||||
- pypy
|
||||
- pypy3
|
||||
env:
|
||||
- PYMONGO=dev DJANGO=dev
|
||||
- PYMONGO=dev DJANGO=1.6.5
|
||||
- PYMONGO=dev DJANGO=1.5.8
|
||||
- PYMONGO=2.7.1 DJANGO=dev
|
||||
- PYMONGO=2.7.1 DJANGO=1.6.5
|
||||
- PYMONGO=2.7.1 DJANGO=1.5.8
|
||||
|
||||
- PYMONGO=2.7.2 DJANGO=dev
|
||||
- PYMONGO=2.7.2 DJANGO=1.7.1
|
||||
- PYMONGO=2.7.2 DJANGO=1.6.8
|
||||
- PYMONGO=2.7.2 DJANGO=1.5.11
|
||||
- PYMONGO=2.8 DJANGO=dev
|
||||
- PYMONGO=2.8 DJANGO=1.7.1
|
||||
- PYMONGO=2.8 DJANGO=1.6.8
|
||||
- PYMONGO=2.8 DJANGO=1.5.11
|
||||
matrix:
|
||||
exclude:
|
||||
- python: "2.6"
|
||||
env: PYMONGO=dev DJANGO=dev
|
||||
- python: "2.6"
|
||||
env: PYMONGO=2.7.1 DJANGO=dev
|
||||
fast_finish: true
|
||||
|
||||
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"
|
||||
|
||||
- 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:
|
||||
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk
|
||||
- if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi
|
||||
- if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; fi
|
||||
- if [[ $DJANGO == 'dev' ]]; then travis_retry pip install https://www.djangoproject.com/download/1.7c2/tarball/; 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 python setup.py install
|
||||
|
||||
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
|
||||
libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev
|
||||
python-tk
|
||||
- if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master;
|
||||
true; fi
|
||||
- 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:
|
||||
- travis_retry python setup.py test
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then 2to3 . -w; fi;
|
||||
- python benchmark.py
|
||||
- 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:
|
||||
irc: "irc.freenode.org#mongoengine"
|
||||
irc: irc.freenode.org#mongoengine
|
||||
branches:
|
||||
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
|
||||
|
12
AUTHORS
12
AUTHORS
@@ -206,3 +206,15 @@ that much better:
|
||||
* 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)
|
||||
|
@@ -79,6 +79,7 @@ Fields
|
||||
.. autoclass:: mongoengine.fields.GenericEmbeddedDocumentField
|
||||
.. autoclass:: mongoengine.fields.DynamicField
|
||||
.. autoclass:: mongoengine.fields.ListField
|
||||
.. autoclass:: mongoengine.fields.EmbeddedDocumentListField
|
||||
.. autoclass:: mongoengine.fields.SortedListField
|
||||
.. autoclass:: mongoengine.fields.DictField
|
||||
.. autoclass:: mongoengine.fields.MapField
|
||||
@@ -95,11 +96,29 @@ Fields
|
||||
.. autoclass:: mongoengine.fields.PointField
|
||||
.. autoclass:: mongoengine.fields.LineStringField
|
||||
.. autoclass:: mongoengine.fields.PolygonField
|
||||
.. autoclass:: mongoengine.fields.MultiPointField
|
||||
.. autoclass:: mongoengine.fields.MultiLineStringField
|
||||
.. autoclass:: mongoengine.fields.MultiPolygonField
|
||||
.. autoclass:: mongoengine.fields.GridFSError
|
||||
.. autoclass:: mongoengine.fields.GridFSProxy
|
||||
.. autoclass:: mongoengine.fields.ImageGridFsProxy
|
||||
.. 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
|
||||
====
|
||||
|
||||
|
@@ -5,6 +5,29 @@ 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
|
||||
@@ -33,7 +56,7 @@ Changes in 0.9.X - DEV
|
||||
- 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() method to provide find_and_modify() like behaviour #677
|
||||
- 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
|
||||
@@ -46,10 +69,19 @@ Changes in 0.9.X - DEV
|
||||
- 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
|
||||
================
|
||||
- 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)
|
||||
- Fix circular import issue with django auth (#531) (#545)
|
||||
|
||||
@@ -62,7 +94,7 @@ Changes in 0.8.5
|
||||
- Fix multi level nested fields getting marked as changed (#523)
|
||||
- Django 1.6 login fix (#522) (#527)
|
||||
- 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 GenericReferenceField serialization order (#499)
|
||||
- Fixed count and none bug (#498)
|
||||
@@ -152,7 +184,7 @@ Changes in 0.8.0
|
||||
- Added `get_next_value` preview for SequenceFields (#319)
|
||||
- Added no_sub_classes context manager and queryset helper (#312)
|
||||
- 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)
|
||||
- Added $setOnInsert support for upserts (#308)
|
||||
- Upserts now possible with just query parameters (#309)
|
||||
@@ -203,7 +235,7 @@ Changes in 0.8.0
|
||||
- Uses getlasterror to test created on updated saves (#163)
|
||||
- Fixed inheritance and unique index creation (#140)
|
||||
- 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 method to document instances (#106)
|
||||
- Added no_dereference context manager (#82) (#61)
|
||||
@@ -285,11 +317,11 @@ Changes in 0.7.2
|
||||
- Update index spec generation so its not destructive (#113)
|
||||
|
||||
Changes in 0.7.1
|
||||
=================
|
||||
================
|
||||
- Fixed index spec inheritance (#111)
|
||||
|
||||
Changes in 0.7.0
|
||||
=================
|
||||
================
|
||||
- Updated queryset.delete so you can use with skip / limit (#107)
|
||||
- Updated index creation allows kwargs to be passed through refs (#104)
|
||||
- Fixed Q object merge edge case (#109)
|
||||
@@ -370,7 +402,7 @@ Changes in 0.6.12
|
||||
- Fixes error with _delta handling DBRefs
|
||||
|
||||
Changes in 0.6.11
|
||||
==================
|
||||
=================
|
||||
- Fixed inconsistency handling None values field attrs
|
||||
- Fixed map_field embedded db_field issue
|
||||
- Fixed .save() _delta issue with DbRefs
|
||||
@@ -450,7 +482,7 @@ Changes in 0.6.1
|
||||
- Fix for replicaSet connections
|
||||
|
||||
Changes in 0.6
|
||||
================
|
||||
==============
|
||||
|
||||
- Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7
|
||||
- Added support for covered indexes when inheritance is off
|
||||
@@ -538,8 +570,8 @@ Changes in v0.5
|
||||
- Updated default collection naming convention
|
||||
- Added Document Mixin support
|
||||
- Fixed queryet __repr__ mid iteration
|
||||
- Added hint() support, so cantell Mongo the proper index to use for the query
|
||||
- Fixed issue with inconsitent setting of _cls breaking inherited referencing
|
||||
- Added hint() support, so can tell Mongo the proper index to use for the query
|
||||
- Fixed issue with inconsistent setting of _cls breaking inherited referencing
|
||||
- Added help_text and verbose_name to fields to help with some form libs
|
||||
- Updated item_frequencies to handle embedded document lookups
|
||||
- Added delta tracking now only sets / unsets explicitly changed fields
|
||||
|
@@ -23,21 +23,32 @@ arguments should be provided::
|
||||
|
||||
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
|
||||
:func:`~mongoengine.connect`::
|
||||
|
||||
connect('project1', host='mongodb://localhost/database_name')
|
||||
|
||||
Note that database name from uri has priority over name
|
||||
in ::func:`~mongoengine.connect`
|
||||
.. note:: Database, username and password from URI string overrides
|
||||
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
|
||||
===========
|
||||
|
||||
MongoEngine supports :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`.
|
||||
To use them, please use a URI style connection and provide the `replicaSet` name in the
|
||||
connection kwargs.
|
||||
MongoEngine supports
|
||||
:class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. To use them,
|
||||
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
|
||||
queries by passing the read_preference ::
|
||||
@@ -77,36 +88,38 @@ to point across databases and collections. Below is an example schema, using
|
||||
meta = {"db_alias": "users-books-db"}
|
||||
|
||||
|
||||
Switch Database Context Manager
|
||||
===============================
|
||||
|
||||
Sometimes you may want to switch the database to query against for a class
|
||||
for example, archiving older data into a separate database for performance
|
||||
reasons.
|
||||
Context Managers
|
||||
================
|
||||
Sometimes you may want to switch the database or collection to query against
|
||||
for a class.
|
||||
For example, archiving older data into a separate database for performance
|
||||
reasons or writing functions that dynamically choose collections to write
|
||||
document to.
|
||||
|
||||
Switch Database
|
||||
---------------
|
||||
The :class:`~mongoengine.context_managers.switch_db` context manager allows
|
||||
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
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
meta = {"db_alias": "user-db"}
|
||||
meta = {"db_alias": "user-db"}
|
||||
|
||||
with switch_db(User, 'archive-user-db') as User:
|
||||
User(name="Ross").save() # Saves the 'archive-user-db'
|
||||
with switch_db(User, 'archive-user-db') as User:
|
||||
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
|
||||
:class:`~mongoengine.context_managers.switch_collection` context manager allows
|
||||
you to change the collection for a given class allowing quick and easy
|
||||
access to the same Group document across collection::
|
||||
Switch Collection
|
||||
-----------------
|
||||
The :class:`~mongoengine.context_managers.switch_collection` context manager
|
||||
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):
|
||||
name = StringField()
|
||||
@@ -115,3 +128,9 @@ access to the same Group document across collection::
|
||||
|
||||
with switch_collection(Group, 'group2000') as Group:
|
||||
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.
|
||||
|
@@ -4,7 +4,7 @@ Defining documents
|
||||
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
|
||||
strict **schema** that the rows follow. MongoDB stores documents in
|
||||
**collections** rather than tables - the principal difference is that no schema
|
||||
**collections** rather than tables --- the principal difference is that no schema
|
||||
is enforced at a database level.
|
||||
|
||||
Defining a document's schema
|
||||
@@ -91,6 +91,12 @@ are as follows:
|
||||
* :class:`~mongoengine.fields.StringField`
|
||||
* :class:`~mongoengine.fields.URLField`
|
||||
* :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
|
||||
---------------
|
||||
@@ -165,15 +171,15 @@ arguments can be set on all fields:
|
||||
size = StringField(max_length=3, choices=SIZE)
|
||||
|
||||
: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)
|
||||
Optional human-readable name for the field - used by form libraries
|
||||
Optional human-readable name for the field -- used by form libraries
|
||||
|
||||
|
||||
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
|
||||
type. :class:`~mongoengine.fields.ListField` takes another field object as its first
|
||||
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
|
||||
(using MongoDB's "unset" operation), effectively nullifying the relationship.
|
||||
: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.
|
||||
:const:`mongoengine.PULL`
|
||||
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`
|
||||
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
|
||||
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
|
||||
@@ -465,8 +471,16 @@ Text indexes may be specified by prefixing the field name with a **$**. ::
|
||||
class Page(Document):
|
||||
title = StringField()
|
||||
rating = StringField()
|
||||
created = DateTimeField()
|
||||
meta = {
|
||||
'indexes': ['title', ('title', '-rating')]
|
||||
'indexes': [
|
||||
'title',
|
||||
('title', '-rating'),
|
||||
{
|
||||
'fields': ['created'],
|
||||
'expireAfterSeconds': 3600
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
If a dictionary is passed then the following options are available:
|
||||
@@ -544,6 +558,9 @@ The following fields will explicitly add a "2dsphere" index:
|
||||
- :class:`~mongoengine.fields.PointField`
|
||||
- :class:`~mongoengine.fields.LineStringField`
|
||||
- :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
|
||||
automatic index but would prefer a compound index. In this example we turn off
|
||||
@@ -655,11 +672,11 @@ Shard keys
|
||||
==========
|
||||
|
||||
If your collection is sharded, then you need to specify the shard key as a tuple,
|
||||
using the :attr:`shard_key` attribute of :attr:`-mongoengine.Document.meta`.
|
||||
using the :attr:`shard_key` attribute of :attr:`~mongoengine.Document.meta`.
|
||||
This ensures that the shard key is sent with the query when calling the
|
||||
:meth:`~mongoengine.document.Document.save` or
|
||||
:meth:`~mongoengine.document.Document.update` method on an existing
|
||||
:class:`-mongoengine.Document` instance::
|
||||
:class:`~mongoengine.Document` instance::
|
||||
|
||||
class LogEntry(Document):
|
||||
machine = StringField()
|
||||
@@ -681,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
|
||||
: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
|
||||
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
|
||||
document.::
|
||||
|
||||
@@ -695,12 +712,12 @@ document.::
|
||||
class DatedPage(Page):
|
||||
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.
|
||||
|
||||
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
|
||||
the expected schema in your database ::
|
||||
|
||||
@@ -723,7 +740,7 @@ Abstract classes
|
||||
|
||||
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
|
||||
: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
|
||||
code DRY::
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Documents instances
|
||||
===================
|
||||
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::
|
||||
|
||||
>>> 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.
|
||||
|
||||
* ``list_field.push(0)`` - *sets* the resulting list
|
||||
* ``del(list_field)`` - *unsets* whole list
|
||||
* ``list_field.push(0)`` --- *sets* the resulting list
|
||||
* ``del(list_field)`` --- *unsets* whole list
|
||||
|
||||
With lists its preferable to use ``Doc.update(push__list_field=0)`` as
|
||||
this stops the whole list being updated - stopping any race conditions.
|
||||
this stops the whole list being updated --- stopping any race conditions.
|
||||
|
||||
.. seealso::
|
||||
:ref:`guide-atomic-updates`
|
||||
@@ -74,7 +74,7 @@ Cascading Saves
|
||||
If your document contains :class:`~mongoengine.fields.ReferenceField` or
|
||||
:class:`~mongoengine.fields.GenericReferenceField` objects, then by default the
|
||||
:meth:`~mongoengine.Document.save` method will 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
|
||||
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'
|
||||
True
|
||||
|
||||
You can also access the document's "primary key" using the :attr:`pk` field; in
|
||||
is an alias to :attr:`id`::
|
||||
You can also access the document's "primary key" using the :attr:`pk` field,
|
||||
it's an alias to :attr:`id`::
|
||||
|
||||
>>> page = Page(title="Another Test Page")
|
||||
>>> page.save()
|
||||
>>> page.id == page.pk
|
||||
True
|
||||
|
||||
.. note::
|
||||
|
||||
|
@@ -17,7 +17,7 @@ fetch documents from the database::
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Filtering queries
|
||||
@@ -42,7 +42,7 @@ syntax::
|
||||
|
||||
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::
|
||||
|
||||
# Only find users whose age is 18 or less
|
||||
@@ -84,19 +84,20 @@ expressions:
|
||||
Geo queries
|
||||
-----------
|
||||
|
||||
There are a few special operators for performing geographical queries. The following
|
||||
were added in 0.8 for: :class:`~mongoengine.fields.PointField`,
|
||||
There are a few special operators for performing geographical queries.
|
||||
The following were added in MongoEngine 0.8 for
|
||||
:class:`~mongoengine.fields.PointField`,
|
||||
:class:`~mongoengine.fields.LineStringField` and
|
||||
:class:`~mongoengine.fields.PolygonField`:
|
||||
|
||||
* ``geo_within`` -- Check if a geometry is within a polygon. For ease of use
|
||||
it accepts either a geojson geometry or just the polygon coordinates eg::
|
||||
* ``geo_within`` -- check if a geometry is within a polygon. For ease of use
|
||||
it accepts either a geojson geometry or just the polygon coordinates eg::
|
||||
|
||||
loc.objects(point__geo_within=[[[40, 5], [40, 6], [41, 6], [40, 5]]])
|
||||
loc.objects(point__geo_within={"type": "Polygon",
|
||||
"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=[<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",
|
||||
"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={"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)
|
||||
|
||||
|
||||
The older 2D indexes are still supported with the
|
||||
:class:`~mongoengine.fields.GeoPointField`:
|
||||
|
||||
* ``within_distance`` -- provide a list containing a point and a maximum
|
||||
distance (e.g. [(41.342, -87.653), 5])
|
||||
* ``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])
|
||||
* ``near`` -- order the documents by how close they are to a given point
|
||||
* ``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)
|
||||
|
||||
.. 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
|
||||
-----------
|
||||
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__``
|
||||
keyword argument::
|
||||
|
||||
@@ -213,12 +214,12 @@ keyword argument::
|
||||
|
||||
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.
|
||||
:meth:`~mongoengine.queryset.QuerySet.limit` and
|
||||
:meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on
|
||||
:class:`~mongoengine.queryset.QuerySet` objects, but the prefered syntax for
|
||||
achieving this is using array-slicing syntax::
|
||||
:class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax
|
||||
is preferred for achieving this::
|
||||
|
||||
# Only the first 5 people
|
||||
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
|
||||
:class:`~mongoengine.queryset.MultipleObjectsReturned`
|
||||
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,
|
||||
: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
|
||||
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
|
||||
@@ -266,9 +267,13 @@ to be created::
|
||||
>>> a.name == b.name and a.age == b.age
|
||||
True
|
||||
|
||||
.. warning::
|
||||
:meth:`~mongoengine.queryset.QuerySet.get_or_create` method is deprecated
|
||||
since :mod:`mongoengine` 0.8.
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
the way to go. To use a custom :class:`~mongoengine.queryset.QuerySet` class on
|
||||
a document, set ``queryset_class`` to the custom class in a
|
||||
:class:`~mongoengine.Document`\ s ``meta`` dictionary::
|
||||
:class:`~mongoengine.Document`'s ``meta`` dictionary::
|
||||
|
||||
class AwesomerQuerySet(QuerySet):
|
||||
|
||||
@@ -491,11 +496,14 @@ Documents may be updated atomically by using the
|
||||
:meth:`~mongoengine.queryset.QuerySet.update_one`,
|
||||
:meth:`~mongoengine.queryset.QuerySet.update` and
|
||||
:meth:`~mongoengine.queryset.QuerySet.modify` methods on a
|
||||
:meth:`~mongoengine.queryset.QuerySet`. There are several different "modifiers"
|
||||
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
|
||||
* ``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
|
||||
* ``dec`` -- decrement a value by a given amount
|
||||
* ``push`` -- append a value to a list
|
||||
@@ -655,3 +663,4 @@ following example shows how the substitutions are made::
|
||||
return comments;
|
||||
}
|
||||
""")
|
||||
|
||||
|
@@ -35,25 +35,25 @@ Available signals include:
|
||||
:class:`~mongoengine.EmbeddedDocument` instance has been completed.
|
||||
|
||||
`pre_save`
|
||||
Called within :meth:`~mongoengine.document.Document.save` prior to performing
|
||||
Called within :meth:`~mongoengine.Document.save` prior to performing
|
||||
any actions.
|
||||
|
||||
`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.
|
||||
|
||||
`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
|
||||
successfully. Passed the additional boolean keyword argument `created` to
|
||||
indicate if the save was an insert or an update.
|
||||
|
||||
`pre_delete`
|
||||
Called within :meth:`~mongoengine.document.Document.delete` prior to
|
||||
Called within :meth:`~mongoengine.Document.delete` prior to
|
||||
attempting the delete operation.
|
||||
|
||||
`post_delete`
|
||||
Called within :meth:`~mongoengine.document.Document.delete` upon successful
|
||||
Called within :meth:`~mongoengine.Document.delete` upon successful
|
||||
deletion of the record.
|
||||
|
||||
`pre_bulk_insert`
|
||||
@@ -145,7 +145,7 @@ cleaner looking while still allowing manual execution of the callback::
|
||||
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
|
||||
reverse deletion.
|
||||
|
||||
|
@@ -46,4 +46,6 @@ Next, start a text search using :attr:`QuerySet.search_text` method::
|
||||
Ordering by text score
|
||||
======================
|
||||
|
||||
::
|
||||
|
||||
objects = News.objects.search('mongo').order_by('$text_score')
|
||||
|
@@ -14,7 +14,7 @@ MongoDB. To install it, simply run
|
||||
MongoEngine.
|
||||
|
||||
: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.
|
||||
|
||||
:doc:`apireference`
|
||||
|
@@ -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)
|
||||
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
|
||||
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
|
||||
|
@@ -5,7 +5,7 @@ Upgrading
|
||||
0.8.7
|
||||
*****
|
||||
|
||||
Calling reload on deleted / nonexistant documents now raises a DoesNotExist
|
||||
Calling reload on deleted / nonexistent documents now raises a DoesNotExist
|
||||
exception.
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ update your code like so: ::
|
||||
[m for m in mammals] # This will return all carnivores
|
||||
|
||||
Len iterates the queryset
|
||||
--------------------------
|
||||
-------------------------
|
||||
|
||||
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
|
||||
|
@@ -15,7 +15,7 @@ import django
|
||||
__all__ = (list(document.__all__) + fields.__all__ + connection.__all__ +
|
||||
list(queryset.__all__) + signals.__all__ + list(errors.__all__))
|
||||
|
||||
VERSION = (0, 8, 7)
|
||||
VERSION = (0, 9, 0)
|
||||
|
||||
|
||||
def get_version():
|
||||
|
@@ -2,8 +2,9 @@ import weakref
|
||||
import functools
|
||||
import itertools
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
|
||||
|
||||
__all__ = ("BaseDict", "BaseList")
|
||||
__all__ = ("BaseDict", "BaseList", "EmbeddedDocumentList")
|
||||
|
||||
|
||||
class BaseDict(dict):
|
||||
@@ -75,6 +76,10 @@ class BaseDict(dict):
|
||||
self._mark_as_changed()
|
||||
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):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).update(*args, **kwargs)
|
||||
@@ -102,7 +107,7 @@ class BaseList(list):
|
||||
if isinstance(instance, (Document, EmbeddedDocument)):
|
||||
self._instance = weakref.proxy(instance)
|
||||
self._name = name
|
||||
return super(BaseList, self).__init__(list_items)
|
||||
super(BaseList, self).__init__(list_items)
|
||||
|
||||
def __getitem__(self, key, *args, **kwargs):
|
||||
value = super(BaseList, self).__getitem__(key)
|
||||
@@ -187,6 +192,167 @@ class BaseList(list):
|
||||
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'])
|
||||
|
@@ -12,11 +12,17 @@ from bson.son import SON
|
||||
from mongoengine import signals
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import (ValidationError, InvalidDocumentError,
|
||||
LookUpError)
|
||||
LookUpError, FieldDoesNotExist)
|
||||
from mongoengine.python_support import PY3, txt_type
|
||||
|
||||
from mongoengine.base.common import get_document, ALLOW_INHERITANCE
|
||||
from mongoengine.base.datastructures import BaseDict, BaseList, StrictDict, SemiStrictDict
|
||||
from mongoengine.base.datastructures import (
|
||||
BaseDict,
|
||||
BaseList,
|
||||
EmbeddedDocumentList,
|
||||
StrictDict,
|
||||
SemiStrictDict
|
||||
)
|
||||
from mongoengine.base.fields import ComplexBaseField
|
||||
|
||||
__all__ = ('BaseDocument', 'NON_FIELD_ERRORS')
|
||||
@@ -26,7 +32,7 @@ NON_FIELD_ERRORS = '__all__'
|
||||
|
||||
class BaseDocument(object):
|
||||
__slots__ = ('_changed_fields', '_initialised', '_created', '_data',
|
||||
'_dynamic_fields', '_auto_id_field', '_db_field_map', '_cls', '__weakref__')
|
||||
'_dynamic_fields', '_auto_id_field', '_db_field_map', '__weakref__')
|
||||
|
||||
_dynamic = False
|
||||
_dynamic_lock = True
|
||||
@@ -54,20 +60,32 @@ class BaseDocument(object):
|
||||
raise TypeError(
|
||||
"Multiple values for keyword argument '" + name + "'")
|
||||
values[name] = value
|
||||
|
||||
__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)
|
||||
|
||||
# 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)()
|
||||
|
||||
_created = values.pop("_created", True)
|
||||
self._data = {}
|
||||
self._dynamic_fields = SON()
|
||||
|
||||
@@ -78,6 +96,9 @@ class BaseDocument(object):
|
||||
value = getattr(self, key, None)
|
||||
setattr(self, key, value)
|
||||
|
||||
if "_cls" not in values:
|
||||
self._cls = self._class_name
|
||||
|
||||
# Set passed values after initialisation
|
||||
if self._dynamic:
|
||||
dynamic_data = {}
|
||||
@@ -226,7 +247,7 @@ class BaseDocument(object):
|
||||
u = self.__str__()
|
||||
except (UnicodeEncodeError, UnicodeDecodeError):
|
||||
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))
|
||||
|
||||
def __str__(self):
|
||||
@@ -238,10 +259,12 @@ class BaseDocument(object):
|
||||
return txt_type('%s object' % self.__class__.__name__)
|
||||
|
||||
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:
|
||||
return self.id == other.id
|
||||
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
|
||||
|
||||
def __ne__(self, other):
|
||||
@@ -264,10 +287,23 @@ class BaseDocument(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def to_mongo(self, use_db_field=True, fields=[]):
|
||||
def get_text_score(self):
|
||||
"""
|
||||
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["_id"] = None
|
||||
data['_cls'] = self._class_name
|
||||
@@ -378,20 +414,21 @@ class BaseDocument(object):
|
||||
"""Converts a document to JSON.
|
||||
: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') if kwargs.has_key(
|
||||
'use_db_field') else True
|
||||
use_db_field = kwargs.pop('use_db_field', True)
|
||||
return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
def from_json(cls, json_data, created=False):
|
||||
"""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):
|
||||
"""expand any dynamic values to their correct types / values"""
|
||||
if not isinstance(value, (dict, list, tuple)):
|
||||
return value
|
||||
|
||||
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
|
||||
|
||||
is_list = False
|
||||
if not hasattr(value, 'items'):
|
||||
is_list = True
|
||||
@@ -415,7 +452,10 @@ class BaseDocument(object):
|
||||
# Convert lists / values so we can watch for any changes on them
|
||||
if (isinstance(value, (list, tuple)) and
|
||||
not isinstance(value, BaseList)):
|
||||
value = BaseList(value, self, name)
|
||||
if issubclass(type(self), EmbeddedDocumentListField):
|
||||
value = EmbeddedDocumentList(value, self, name)
|
||||
else:
|
||||
value = BaseList(value, self, name)
|
||||
elif isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||
value = BaseDict(value, self, name)
|
||||
|
||||
@@ -614,9 +654,11 @@ class BaseDocument(object):
|
||||
return cls._meta.get('collection', None)
|
||||
|
||||
@classmethod
|
||||
def _from_son(cls, son, _auto_dereference=True, only_fields=[]):
|
||||
def _from_son(cls, son, _auto_dereference=True, only_fields=None, created=False):
|
||||
"""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
|
||||
# class if unavailable
|
||||
@@ -664,7 +706,7 @@ class BaseDocument(object):
|
||||
if cls.STRICT:
|
||||
data = dict((k, v)
|
||||
for k, v in data.iteritems() if k in cls._fields)
|
||||
obj = cls(__auto_convert=False, _created=False, __only_fields=only_fields, **data)
|
||||
obj = cls(__auto_convert=False, _created=created, __only_fields=only_fields, **data)
|
||||
obj._changed_fields = changed_fields
|
||||
if not _auto_dereference:
|
||||
obj._fields = fields
|
||||
@@ -687,7 +729,7 @@ class BaseDocument(object):
|
||||
|
||||
spec_fields = [v['fields']
|
||||
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):
|
||||
if v['fields'] in spec_fields:
|
||||
index_specs[spec_fields.index(v['fields'])].update(v)
|
||||
@@ -718,6 +760,9 @@ class BaseDocument(object):
|
||||
ALLOW_INHERITANCE)
|
||||
include_cls = (allow_inheritance and not spec.get('sparse', False) and
|
||||
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:
|
||||
spec.pop('cls')
|
||||
for key in spec['fields']:
|
||||
@@ -778,10 +823,9 @@ class BaseDocument(object):
|
||||
"""
|
||||
unique_indexes = []
|
||||
for field_name, field in cls._fields.items():
|
||||
sparse = False
|
||||
sparse = field.sparse
|
||||
# Generate a list of indexes needed by uniqueness constraints
|
||||
if field.unique:
|
||||
field.required = True
|
||||
unique_fields = [field.db_field]
|
||||
|
||||
# Add any unique_with fields to the back of the index spec
|
||||
@@ -809,6 +853,9 @@ class BaseDocument(object):
|
||||
index = {'fields': fields, 'unique': True, 'sparse': sparse}
|
||||
unique_indexes.append(index)
|
||||
|
||||
if field.__class__.__name__ == "ListField":
|
||||
field = field.field
|
||||
|
||||
# Grab any embedded document field unique indexes
|
||||
if (field.__class__.__name__ == "EmbeddedDocumentField" and
|
||||
field.document_type != cls):
|
||||
@@ -878,6 +925,19 @@ class BaseDocument(object):
|
||||
elif cls._dynamic:
|
||||
DynamicField = _import_class('DynamicField')
|
||||
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:
|
||||
raise LookUpError('Cannot resolve field "%s"'
|
||||
% field_name)
|
||||
|
@@ -9,7 +9,9 @@ from mongoengine.common import _import_class
|
||||
from mongoengine.errors import ValidationError
|
||||
|
||||
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")
|
||||
@@ -37,7 +39,7 @@ class BaseField(object):
|
||||
def __init__(self, db_field=None, name=None, required=False, default=None,
|
||||
unique=False, unique_with=None, primary_key=False,
|
||||
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
|
||||
(defaults to the name of the field)
|
||||
@@ -60,6 +62,10 @@ class BaseField(object):
|
||||
model forms from the document model.
|
||||
:param help_text: (optional) The help text for this field and is often
|
||||
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'
|
||||
|
||||
@@ -75,6 +81,8 @@ class BaseField(object):
|
||||
self.choices = choices
|
||||
self.verbose_name = verbose_name
|
||||
self.help_text = help_text
|
||||
self.null = null
|
||||
self.sparse = sparse
|
||||
|
||||
# Adjust the appropriate creation counter, and save our local copy.
|
||||
if self.db_field == '_id':
|
||||
@@ -100,10 +108,13 @@ class BaseField(object):
|
||||
|
||||
# If setting to None and theres a default
|
||||
# Then set the value to the default value
|
||||
if value is None and self.default is not None:
|
||||
value = self.default
|
||||
if callable(value):
|
||||
value = value()
|
||||
if value is None:
|
||||
if self.null:
|
||||
value = None
|
||||
elif self.default is not None:
|
||||
value = self.default
|
||||
if callable(value):
|
||||
value = value()
|
||||
|
||||
if instance._initialised:
|
||||
try:
|
||||
@@ -149,21 +160,23 @@ class BaseField(object):
|
||||
def _validate(self, value, **kwargs):
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
# check choices
|
||||
|
||||
# Check the Choices Constraint
|
||||
if self.choices:
|
||||
is_cls = isinstance(value, (Document, EmbeddedDocument))
|
||||
value_to_check = value.__class__ if is_cls else value
|
||||
err_msg = 'an instance' if is_cls else 'one'
|
||||
|
||||
choice_list = self.choices
|
||||
if isinstance(self.choices[0], (list, tuple)):
|
||||
option_keys = [k for k, v in self.choices]
|
||||
if value_to_check not in option_keys:
|
||||
msg = ('Value must be %s of %s' %
|
||||
(err_msg, unicode(option_keys)))
|
||||
self.error(msg)
|
||||
elif value_to_check not in self.choices:
|
||||
msg = ('Value must be %s of %s' %
|
||||
(err_msg, unicode(self.choices)))
|
||||
self.error(msg)
|
||||
choice_list = [k for k, v in self.choices]
|
||||
|
||||
# Choices which are other types of Documents
|
||||
if isinstance(value, (Document, EmbeddedDocument)):
|
||||
if not any(isinstance(value, c) for c in choice_list):
|
||||
self.error(
|
||||
'Value must be instance of %s' % unicode(choice_list)
|
||||
)
|
||||
# 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
|
||||
if self.validation is not None:
|
||||
@@ -199,6 +212,7 @@ class ComplexBaseField(BaseField):
|
||||
|
||||
ReferenceField = _import_class('ReferenceField')
|
||||
GenericReferenceField = _import_class('GenericReferenceField')
|
||||
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
|
||||
dereference = (self._auto_dereference and
|
||||
(self.field is None or isinstance(self.field,
|
||||
(GenericReferenceField, ReferenceField))))
|
||||
@@ -215,9 +229,12 @@ class ComplexBaseField(BaseField):
|
||||
value = super(ComplexBaseField, self).__get__(instance, owner)
|
||||
|
||||
# Convert lists / values so we can watch for any changes on them
|
||||
if (isinstance(value, (list, tuple)) and
|
||||
not isinstance(value, BaseList)):
|
||||
value = BaseList(value, instance, self.name)
|
||||
if isinstance(value, (list, tuple)):
|
||||
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)
|
||||
instance._data[self.name] = value
|
||||
elif isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||
value = BaseDict(value, instance, self.name)
|
||||
@@ -419,6 +436,7 @@ class ObjectIdField(BaseField):
|
||||
class GeoJsonBaseField(BaseField):
|
||||
|
||||
"""A geo json field storing a geojson style object.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
|
||||
@@ -427,8 +445,8 @@ class GeoJsonBaseField(BaseField):
|
||||
|
||||
def __init__(self, auto_index=True, *args, **kwargs):
|
||||
"""
|
||||
:param auto_index: Automatically create a "2dsphere" index. Defaults
|
||||
to `True`.
|
||||
:param bool auto_index: Automatically create a "2dsphere" index.\
|
||||
Defaults to `True`.
|
||||
"""
|
||||
self._name = "%sField" % self._type
|
||||
if not auto_index:
|
||||
@@ -457,7 +475,7 @@ class GeoJsonBaseField(BaseField):
|
||||
if error:
|
||||
self.error(error)
|
||||
|
||||
def _validate_polygon(self, value):
|
||||
def _validate_polygon(self, value, top_level=True):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return 'Polygons must contain list of linestrings'
|
||||
|
||||
@@ -475,7 +493,10 @@ class GeoJsonBaseField(BaseField):
|
||||
if error and error not in errors:
|
||||
errors.append(error)
|
||||
if errors:
|
||||
return "Invalid Polygon:\n%s" % ", ".join(errors)
|
||||
if top_level:
|
||||
return "Invalid Polygon:\n%s" % ", ".join(errors)
|
||||
else:
|
||||
return "%s" % ", ".join(errors)
|
||||
|
||||
def _validate_linestring(self, value, top_level=True):
|
||||
"""Validates a linestring"""
|
||||
@@ -509,6 +530,66 @@ class GeoJsonBaseField(BaseField):
|
||||
not isinstance(value[1], (float, int))):
|
||||
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):
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
|
@@ -46,6 +46,11 @@ class DocumentMetaclass(type):
|
||||
elif hasattr(base, '_meta'):
|
||||
meta.merge(base._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
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
_class_registry_cache = {}
|
||||
_field_list_cache = []
|
||||
|
||||
|
||||
def _import_class(cls_name):
|
||||
@@ -20,13 +21,16 @@ def _import_class(cls_name):
|
||||
|
||||
doc_classes = ('Document', 'DynamicEmbeddedDocument', 'EmbeddedDocument',
|
||||
'MapReduceDocument')
|
||||
field_classes = ('DictField', 'DynamicField', 'EmbeddedDocumentField',
|
||||
'FileField', 'GenericReferenceField',
|
||||
'GenericEmbeddedDocumentField', 'GeoPointField',
|
||||
'PointField', 'LineStringField', 'ListField',
|
||||
'PolygonField', 'ReferenceField', 'StringField',
|
||||
'CachedReferenceField',
|
||||
'ComplexBaseField', 'GeoJsonBaseField')
|
||||
|
||||
# Field Classes
|
||||
if not _field_list_cache:
|
||||
from mongoengine.fields import __all__ as fields
|
||||
_field_list_cache.extend(fields)
|
||||
from mongoengine.base.fields import __all__ as fields
|
||||
_field_list_cache.extend(fields)
|
||||
|
||||
field_classes = _field_list_cache
|
||||
|
||||
queryset_classes = ('OperationError',)
|
||||
deref_classes = ('DeReference',)
|
||||
|
||||
|
@@ -18,7 +18,7 @@ _connections = {}
|
||||
_dbs = {}
|
||||
|
||||
|
||||
def register_connection(alias, name, host=None, port=None,
|
||||
def register_connection(alias, name=None, host=None, port=None,
|
||||
read_preference=False,
|
||||
username=None, password=None, authentication_source=None,
|
||||
**kwargs):
|
||||
@@ -40,7 +40,7 @@ def register_connection(alias, name, host=None, port=None,
|
||||
global _connection_settings
|
||||
|
||||
conn_settings = {
|
||||
'name': name,
|
||||
'name': name or 'test',
|
||||
'host': host or 'localhost',
|
||||
'port': port or 27017,
|
||||
'read_preference': read_preference,
|
||||
@@ -111,13 +111,14 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
||||
|
||||
try:
|
||||
connection = None
|
||||
connection_settings_iterator = ((alias, settings.copy()) for alias, settings in _connection_settings.iteritems())
|
||||
for alias, connection_settings in connection_settings_iterator:
|
||||
# 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(alias, None):
|
||||
connection = _connections[alias]
|
||||
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)
|
||||
@@ -144,7 +145,7 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
||||
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.
|
||||
|
||||
Connection settings may be provided here as well if the database is not
|
||||
|
@@ -1,6 +1,9 @@
|
||||
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 connection import get_db
|
||||
from queryset import QuerySet
|
||||
@@ -180,11 +183,18 @@ class DeReference(object):
|
||||
return self.object_map.get(items['_ref'].id, items)
|
||||
elif '_cls' in 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)
|
||||
if _cls is not None:
|
||||
doc._data['_cls'] = _cls
|
||||
return doc
|
||||
|
||||
if not hasattr(items, 'items'):
|
||||
is_list = True
|
||||
list_type = BaseList
|
||||
if isinstance(items, EmbeddedDocumentList):
|
||||
list_type = EmbeddedDocumentList
|
||||
as_tuple = isinstance(items, tuple)
|
||||
iterator = enumerate(items)
|
||||
data = []
|
||||
@@ -221,7 +231,7 @@ class DeReference(object):
|
||||
|
||||
if instance and name:
|
||||
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)
|
||||
depth += 1
|
||||
return data
|
||||
|
@@ -3,7 +3,11 @@ from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import UserManager
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
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 _
|
||||
|
||||
|
||||
|
@@ -23,7 +23,7 @@ class MongoTestCase(TestCase):
|
||||
|
||||
def dropCollections(self):
|
||||
for collection in self.db.collection_names():
|
||||
if collection == 'system.indexes':
|
||||
if collection.startswith('system.'):
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
||||
|
||||
|
@@ -9,10 +9,17 @@ from bson import ObjectId
|
||||
from bson.dbref import DBRef
|
||||
from mongoengine import signals
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.base import (DocumentMetaclass, TopLevelDocumentMetaclass,
|
||||
BaseDocument, BaseDict, BaseList,
|
||||
ALLOW_INHERITANCE, get_document)
|
||||
from mongoengine.errors import ValidationError
|
||||
from mongoengine.base import (
|
||||
DocumentMetaclass,
|
||||
TopLevelDocumentMetaclass,
|
||||
BaseDocument,
|
||||
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
|
||||
@@ -76,6 +83,12 @@ class EmbeddedDocument(BaseDocument):
|
||||
def __ne__(self, 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):
|
||||
|
||||
@@ -113,7 +126,7 @@ class Document(BaseDocument):
|
||||
a **+** or **-** sign.
|
||||
|
||||
Automatic index creation can be disabled by specifying
|
||||
attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to
|
||||
:attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to
|
||||
False then indexes will not be created by MongoEngine. This is useful in
|
||||
production systems where index creation is performed as part of a
|
||||
deployment system.
|
||||
@@ -143,13 +156,6 @@ class Document(BaseDocument):
|
||||
return property(fget, fset)
|
||||
pk = pk()
|
||||
|
||||
@property
|
||||
def text_score(self):
|
||||
"""
|
||||
Used for text searchs
|
||||
"""
|
||||
return self._data.get('text_score')
|
||||
|
||||
@classmethod
|
||||
def _get_db(cls):
|
||||
"""Some Model using other db_alias"""
|
||||
@@ -192,6 +198,44 @@ class Document(BaseDocument):
|
||||
cls.ensure_indexes()
|
||||
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,
|
||||
write_concern=None, cascade=None, cascade_kwargs=None,
|
||||
_refs=None, save_condition=None, **kwargs):
|
||||
@@ -219,6 +263,7 @@ class Document(BaseDocument):
|
||||
: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
|
||||
In existing documents it only saves changed fields using
|
||||
set / unset. Saves are cascaded and any
|
||||
@@ -253,6 +298,8 @@ class Document(BaseDocument):
|
||||
|
||||
try:
|
||||
collection = self._get_collection()
|
||||
if self._meta.get('auto_create_index', True):
|
||||
self.ensure_indexes()
|
||||
if created:
|
||||
if force_insert:
|
||||
object_id = collection.insert(doc, **write_concern)
|
||||
@@ -424,10 +471,11 @@ class Document(BaseDocument):
|
||||
user.switch_db('archive-db')
|
||||
user.save()
|
||||
|
||||
If you need to read from another database see
|
||||
:class:`~mongoengine.context_managers.switch_db`
|
||||
:param str db_alias: The database alias to use for saving the document
|
||||
|
||||
: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:
|
||||
collection = cls._get_collection()
|
||||
@@ -450,11 +498,12 @@ class Document(BaseDocument):
|
||||
user.switch_collection('old-users')
|
||||
user.save()
|
||||
|
||||
If you need to read from another database see
|
||||
:class:`~mongoengine.context_managers.switch_db`
|
||||
|
||||
:param collection_name: The database alias to use for saving the
|
||||
:param str collection_name: The database alias to use for saving the
|
||||
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:
|
||||
collection = cls._get_collection()
|
||||
@@ -505,7 +554,13 @@ class Document(BaseDocument):
|
||||
|
||||
for field in self._fields_ordered:
|
||||
if not fields or field in fields:
|
||||
setattr(self, field, self._reload(field, obj[field]))
|
||||
try:
|
||||
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._created = False
|
||||
@@ -518,6 +573,9 @@ class Document(BaseDocument):
|
||||
if isinstance(value, BaseDict):
|
||||
value = [(k, self._reload(k, v)) for k, v in value.items()]
|
||||
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):
|
||||
value = [self._reload(key, v) for v in value]
|
||||
value = BaseList(value, self, key)
|
||||
@@ -594,7 +652,9 @@ class Document(BaseDocument):
|
||||
index_cls = cls._meta.get('index_cls', True)
|
||||
|
||||
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
|
||||
|
||||
# determine if an index which we are creating includes
|
||||
@@ -631,7 +691,7 @@ class Document(BaseDocument):
|
||||
if cls._meta.get('abstract'):
|
||||
return []
|
||||
|
||||
# get all the base classes, subclasses and sieblings
|
||||
# get all the base classes, subclasses and siblings
|
||||
classes = []
|
||||
|
||||
def get_classes(cls):
|
||||
|
@@ -5,7 +5,8 @@ from mongoengine.python_support import txt_type
|
||||
|
||||
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
|
||||
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
|
||||
'OperationError', 'NotUniqueError', 'ValidationError')
|
||||
'OperationError', 'NotUniqueError', 'FieldDoesNotExist',
|
||||
'ValidationError')
|
||||
|
||||
|
||||
class NotRegistered(Exception):
|
||||
@@ -40,6 +41,10 @@ class NotUniqueError(OperationError):
|
||||
pass
|
||||
|
||||
|
||||
class FieldDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(AssertionError):
|
||||
"""Validation exception.
|
||||
|
||||
|
@@ -39,12 +39,13 @@ __all__ = [
|
||||
'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField',
|
||||
'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField',
|
||||
'GenericEmbeddedDocumentField', 'DynamicField', 'ListField',
|
||||
'SortedListField', 'DictField', 'MapField', 'ReferenceField',
|
||||
'CachedReferenceField', 'GenericReferenceField', 'BinaryField',
|
||||
'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy',
|
||||
'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField',
|
||||
'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField',
|
||||
'GeoJsonBaseField']
|
||||
'SortedListField', 'EmbeddedDocumentListField', 'DictField',
|
||||
'MapField', 'ReferenceField', 'CachedReferenceField',
|
||||
'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy',
|
||||
'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField',
|
||||
'GeoPointField', 'PointField', 'LineStringField', 'PolygonField',
|
||||
'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField',
|
||||
'MultiPolygonField', 'GeoJsonBaseField']
|
||||
|
||||
|
||||
RECURSIVE_REFERENCE_CONSTANT = 'self'
|
||||
@@ -160,8 +161,8 @@ class EmailField(StringField):
|
||||
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"
|
||||
# quoted-string
|
||||
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"'
|
||||
# domain
|
||||
r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?\.)+[A-Z]{2,6}$', re.IGNORECASE
|
||||
# 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):
|
||||
@@ -290,7 +291,7 @@ class DecimalField(BaseField):
|
||||
:param max_value: Validation rule for the maximum acceptable value.
|
||||
:param force_string: Store as a string.
|
||||
: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_DOWN (towards zero)
|
||||
@@ -307,7 +308,7 @@ class DecimalField(BaseField):
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.force_string = force_string
|
||||
self.precision = decimal.Decimal(".%s" % ("0" * precision))
|
||||
self.precision = precision
|
||||
self.rounding = rounding
|
||||
|
||||
super(DecimalField, self).__init__(**kwargs)
|
||||
@@ -321,7 +322,7 @@ class DecimalField(BaseField):
|
||||
value = decimal.Decimal("%s" % value)
|
||||
except decimal.InvalidOperation:
|
||||
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, use_db_field=True):
|
||||
if value is None:
|
||||
@@ -374,11 +375,11 @@ class DateTimeField(BaseField):
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
need accurate microsecond support.
|
||||
"""
|
||||
@@ -509,7 +510,7 @@ class ComplexDateTimeField(StringField):
|
||||
def __get__(self, instance, owner):
|
||||
data = super(ComplexDateTimeField, self).__get__(instance, owner)
|
||||
if data is None:
|
||||
return datetime.datetime.now()
|
||||
return None if self.null else datetime.datetime.now()
|
||||
if isinstance(data, datetime.datetime):
|
||||
return data
|
||||
return self._convert_from_string(data)
|
||||
@@ -637,7 +638,7 @@ class DynamicField(BaseField):
|
||||
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
|
||||
|
||||
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):
|
||||
@@ -727,6 +728,32 @@ class ListField(ComplexBaseField):
|
||||
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):
|
||||
|
||||
"""A ListField that sorts the contents of its list before writing to
|
||||
@@ -826,6 +853,10 @@ class DictField(ComplexBaseField):
|
||||
return StringField().prepare_query_value(op, value)
|
||||
|
||||
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 super(DictField, self).prepare_query_value(op, value)
|
||||
@@ -854,7 +885,7 @@ class ReferenceField(BaseField):
|
||||
|
||||
Use the `reverse_delete_rule` to handle what should happen if the document
|
||||
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.
|
||||
|
||||
The options are:
|
||||
@@ -878,7 +909,7 @@ class ReferenceField(BaseField):
|
||||
Bar.register_delete_rule(Foo, 'bar', NULLIFY)
|
||||
|
||||
.. 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.
|
||||
|
||||
.. versionchanged:: 0.5 added `reverse_delete_rule`
|
||||
@@ -1326,6 +1357,7 @@ class GridFSProxy(object):
|
||||
def new_file(self, **kwargs):
|
||||
self.newfile = self.fs.new_file(**kwargs)
|
||||
self.grid_id = self.newfile._id
|
||||
self._mark_as_changed()
|
||||
|
||||
def put(self, file_obj, **kwargs):
|
||||
if self.grid_id:
|
||||
@@ -1644,7 +1676,7 @@ class ImageField(FileField):
|
||||
|
||||
class SequenceField(BaseField):
|
||||
|
||||
"""Provides a sequental counter see:
|
||||
"""Provides a sequential counter see:
|
||||
http://www.mongodb.org/display/DOCS/Object+IDs#ObjectIDs-SequenceNumbers
|
||||
|
||||
.. note::
|
||||
@@ -1745,7 +1777,7 @@ class SequenceField(BaseField):
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
"""
|
||||
This method is overriden in order to convert the query value into to required
|
||||
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.
|
||||
"""
|
||||
@@ -1855,6 +1887,7 @@ class PointField(GeoJsonBaseField):
|
||||
to set the value.
|
||||
|
||||
Requires mongodb >= 2.4
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
_type = "Point"
|
||||
@@ -1874,6 +1907,7 @@ class LineStringField(GeoJsonBaseField):
|
||||
You can either pass a dict with the full information or a list of points.
|
||||
|
||||
Requires mongodb >= 2.4
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
_type = "LineString"
|
||||
@@ -1896,6 +1930,77 @@ class PolygonField(GeoJsonBaseField):
|
||||
holes.
|
||||
|
||||
Requires mongodb >= 2.4
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
_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"
|
||||
|
@@ -66,7 +66,6 @@ class BaseQuerySet(object):
|
||||
self._as_pymongo = False
|
||||
self._as_pymongo_coerce = False
|
||||
self._search_text = None
|
||||
self._include_text_scores = False
|
||||
|
||||
# If inheritance is allowed, only return instances and instances of
|
||||
# subclasses of the class being used
|
||||
@@ -82,6 +81,7 @@ class BaseQuerySet(object):
|
||||
self._skip = None
|
||||
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,
|
||||
read_preference=None, **query):
|
||||
@@ -158,7 +158,8 @@ class BaseQuerySet(object):
|
||||
if queryset._as_pymongo:
|
||||
return queryset._get_as_pymongo(queryset._cursor[key])
|
||||
return queryset._document._from_son(queryset._cursor[key],
|
||||
_auto_dereference=self._auto_dereference, only_fields=self.only_fields)
|
||||
_auto_dereference=self._auto_dereference, only_fields=self.only_fields)
|
||||
|
||||
raise AttributeError
|
||||
|
||||
def __iter__(self):
|
||||
@@ -191,7 +192,7 @@ class BaseQuerySet(object):
|
||||
"""
|
||||
return self.__call__(*q_objs, **query)
|
||||
|
||||
def search_text(self, text, language=None, include_text_scores=False):
|
||||
def search_text(self, text, language=None):
|
||||
"""
|
||||
Start a text search, using text indexes.
|
||||
Require: MongoDB server version 2.6+.
|
||||
@@ -200,14 +201,11 @@ class BaseQuerySet(object):
|
||||
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>`.
|
||||
|
||||
:param include_text_scores: If True, automaticaly add a text_score attribute to Document.
|
||||
|
||||
"""
|
||||
queryset = self.clone()
|
||||
if queryset._search_text:
|
||||
raise OperationError(
|
||||
"Is not possible to use search_text two times.")
|
||||
"It is not possible to use search_text two times.")
|
||||
|
||||
query_kwargs = SON({'$search': text})
|
||||
if language:
|
||||
@@ -217,7 +215,6 @@ class BaseQuerySet(object):
|
||||
queryset._mongo_query = None
|
||||
queryset._cursor_obj = None
|
||||
queryset._search_text = text
|
||||
queryset._include_text_scores = include_text_scores
|
||||
|
||||
return queryset
|
||||
|
||||
@@ -271,7 +268,7 @@ class BaseQuerySet(object):
|
||||
.. note:: This requires two separate operations and therefore a
|
||||
race condition exists. Because there are no transactions in
|
||||
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
|
||||
|
||||
:param write_concern: optional extra keyword arguments used if we
|
||||
@@ -383,7 +380,7 @@ class BaseQuerySet(object):
|
||||
self._document, documents=results, loaded=True)
|
||||
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.
|
||||
|
||||
:param with_limit_and_skip (optional): take any :meth:`limit` or
|
||||
@@ -405,6 +402,7 @@ class BaseQuerySet(object):
|
||||
will force an fsync on the primary server.
|
||||
:param _from_doc_delete: True when called from document delete therefore
|
||||
signals will have been triggered so don't loop.
|
||||
|
||||
:returns number of deleted documents
|
||||
"""
|
||||
queryset = self.clone()
|
||||
@@ -434,6 +432,8 @@ class BaseQuerySet(object):
|
||||
# references
|
||||
for rule_entry in delete_rules:
|
||||
document_cls, field_name = rule_entry
|
||||
if document_cls._meta.get('abstract'):
|
||||
continue
|
||||
rule = doc._meta['delete_rules'][rule_entry]
|
||||
if rule == DENY and document_cls.objects(
|
||||
**{field_name + '__in': self}).count() > 0:
|
||||
@@ -443,6 +443,8 @@ class BaseQuerySet(object):
|
||||
|
||||
for rule_entry in delete_rules:
|
||||
document_cls, field_name = rule_entry
|
||||
if document_cls._meta.get('abstract'):
|
||||
continue
|
||||
rule = doc._meta['delete_rules'][rule_entry]
|
||||
if rule == CASCADE:
|
||||
ref_q = document_cls.objects(**{field_name + '__in': self})
|
||||
@@ -458,7 +460,7 @@ class BaseQuerySet(object):
|
||||
write_concern=write_concern,
|
||||
**{'pull_all__%s' % field_name: self})
|
||||
|
||||
result = 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,
|
||||
@@ -619,7 +621,9 @@ class BaseQuerySet(object):
|
||||
doc_map[doc['_id']] = self._get_as_pymongo(doc)
|
||||
else:
|
||||
for doc in docs:
|
||||
doc_map[doc['_id']] = self._document._from_son(doc, only_fields=self.only_fields)
|
||||
doc_map[doc['_id']] = self._document._from_son(doc,
|
||||
only_fields=self.only_fields,
|
||||
_auto_dereference=self._auto_dereference)
|
||||
|
||||
return doc_map
|
||||
|
||||
@@ -672,7 +676,7 @@ class BaseQuerySet(object):
|
||||
'_timeout', '_class_check', '_slave_okay', '_read_preference',
|
||||
'_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce',
|
||||
'_limit', '_skip', '_hint', '_auto_dereference',
|
||||
'_search_text', '_include_text_scores', 'only_fields')
|
||||
'_search_text', 'only_fields', '_max_time_ms')
|
||||
|
||||
for prop in copy_props:
|
||||
val = getattr(self, prop)
|
||||
@@ -758,14 +762,29 @@ class BaseQuerySet(object):
|
||||
distinct = self._dereference(queryset._cursor.distinct(field), 1,
|
||||
name=field, instance=self._document)
|
||||
|
||||
# We may need to cast to the correct type eg.
|
||||
# ListField(EmbeddedDocumentField)
|
||||
doc_field = getattr(
|
||||
self._document._fields.get(field), "field", None)
|
||||
instance = getattr(doc_field, "document_type", False)
|
||||
doc_field = self._document._fields.get(field.split('.', 1)[0])
|
||||
instance = False
|
||||
# We may need to cast to the correct type eg. ListField(EmbeddedDocumentField)
|
||||
EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
|
||||
GenericEmbeddedDocumentField = _import_class(
|
||||
'GenericEmbeddedDocumentField')
|
||||
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)
|
||||
# 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]
|
||||
@@ -969,6 +988,13 @@ class BaseQuerySet(object):
|
||||
queryset._as_pymongo_coerce = coerce_types
|
||||
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
|
||||
|
||||
def to_json(self, *args, **kwargs):
|
||||
@@ -982,8 +1008,8 @@ class BaseQuerySet(object):
|
||||
|
||||
def aggregate(self, *pipeline, **kwargs):
|
||||
"""
|
||||
Perform a aggreggate function based in your queryset params
|
||||
:param pipeline: list of agreggation commands,
|
||||
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
|
||||
@@ -1331,6 +1357,7 @@ class BaseQuerySet(object):
|
||||
return self._get_as_pymongo(raw_doc)
|
||||
doc = self._document._from_son(raw_doc,
|
||||
_auto_dereference=self._auto_dereference, only_fields=self.only_fields)
|
||||
|
||||
if self._scalar:
|
||||
return self._get_scalar(doc)
|
||||
|
||||
@@ -1339,6 +1366,7 @@ class BaseQuerySet(object):
|
||||
def rewind(self):
|
||||
"""Rewind the cursor to its unevaluated state.
|
||||
|
||||
|
||||
.. versionadded:: 0.3
|
||||
"""
|
||||
self._iter = False
|
||||
@@ -1366,11 +1394,11 @@ class BaseQuerySet(object):
|
||||
if self._loaded_fields:
|
||||
cursor_args['fields'] = self._loaded_fields.as_dict()
|
||||
|
||||
if self._include_text_scores:
|
||||
if self._search_text:
|
||||
if 'fields' not in cursor_args:
|
||||
cursor_args['fields'] = {}
|
||||
|
||||
cursor_args['fields']['text_score'] = {'$meta': "textScore"}
|
||||
cursor_args['fields']['_text_score'] = {'$meta': "textScore"}
|
||||
|
||||
return cursor_args
|
||||
|
||||
@@ -1413,8 +1441,11 @@ class BaseQuerySet(object):
|
||||
def _query(self):
|
||||
if self._mongo_query is None:
|
||||
self._mongo_query = self._query_obj.to_query(self._document)
|
||||
if self._class_check:
|
||||
self._mongo_query.update(self._initial_query)
|
||||
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)
|
||||
return self._mongo_query
|
||||
|
||||
@property
|
||||
@@ -1582,9 +1613,7 @@ class BaseQuerySet(object):
|
||||
continue
|
||||
|
||||
if key == '$text_score':
|
||||
# automatically set to include text scores
|
||||
self._include_text_scores = True
|
||||
key_list.append(('text_score', {'$meta': "textScore"}))
|
||||
key_list.append(('_text_score', {'$meta': "textScore"}))
|
||||
continue
|
||||
|
||||
direction = pymongo.ASCENDING
|
||||
@@ -1697,6 +1726,13 @@ class BaseQuerySet(object):
|
||||
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
|
||||
def ensure_index(self, **kwargs):
|
||||
"""Deprecated use :func:`Document.ensure_index`"""
|
||||
|
@@ -94,7 +94,7 @@ class QuerySet(BaseQuerySet):
|
||||
except StopIteration:
|
||||
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.
|
||||
|
||||
:param with_limit_and_skip (optional): take any :meth:`limit` or
|
||||
|
@@ -11,7 +11,7 @@ __all__ = ('query', 'update')
|
||||
|
||||
|
||||
COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
||||
'all', 'size', 'exists', 'not', 'elemMatch')
|
||||
'all', 'size', 'exists', 'not', 'elemMatch', 'type')
|
||||
GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
|
||||
'within_box', 'within_polygon', 'near', 'near_sphere',
|
||||
'max_distance', 'geo_within', 'geo_within_box',
|
||||
@@ -26,7 +26,7 @@ MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
|
||||
|
||||
UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push',
|
||||
'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):
|
||||
@@ -160,7 +160,7 @@ def query(_doc_cls=None, _field_operation=False, **query):
|
||||
if isinstance(v, list):
|
||||
value = [{k: val} for val in v]
|
||||
if '$and' in mongo_query.keys():
|
||||
mongo_query['$and'].append(value)
|
||||
mongo_query['$and'].extend(value)
|
||||
else:
|
||||
mongo_query['$and'] = value
|
||||
|
||||
|
@@ -29,7 +29,7 @@ class DuplicateQueryConditionsError(InvalidQueryError):
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
@@ -1 +1,2 @@
|
||||
pymongo>=2.7.1
|
||||
nose
|
||||
|
2
setup.py
2
setup.py
@@ -38,7 +38,7 @@ CLASSIFIERS = [
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.6.6",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.2",
|
||||
|
@@ -36,9 +36,9 @@ class ClassMethodsTest(unittest.TestCase):
|
||||
def test_definition(self):
|
||||
"""Ensure that document may be defined using fields.
|
||||
"""
|
||||
self.assertEqual(['age', 'id', 'name'],
|
||||
self.assertEqual(['_cls', 'age', 'id', 'name'],
|
||||
sorted(self.Person._fields.keys()))
|
||||
self.assertEqual(["IntField", "ObjectIdField", "StringField"],
|
||||
self.assertEqual(["IntField", "ObjectIdField", "StringField", "StringField"],
|
||||
sorted([x.__class__.__name__ for x in
|
||||
self.Person._fields.values()]))
|
||||
|
||||
|
@@ -81,6 +81,13 @@ class DynamicTest(unittest.TestCase):
|
||||
obj = collection.find_one()
|
||||
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):
|
||||
"""Ensure we can query dynamic fields"""
|
||||
p = self.Person()
|
||||
|
@@ -18,7 +18,7 @@ __all__ = ("IndexesTest", )
|
||||
class IndexesTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.connection = connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
class Person(Document):
|
||||
@@ -175,6 +175,16 @@ class IndexesTest(unittest.TestCase):
|
||||
info = A._get_collection().index_information()
|
||||
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):
|
||||
|
||||
class MyDoc(Document):
|
||||
@@ -485,9 +495,12 @@ class IndexesTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10)
|
||||
|
||||
def invalid_index():
|
||||
BlogPost.objects.hint('tags')
|
||||
self.assertRaises(TypeError, invalid_index)
|
||||
if pymongo.version >= '2.8':
|
||||
self.assertEqual(BlogPost.objects.hint('tags').count(), 10)
|
||||
else:
|
||||
def invalid_index():
|
||||
BlogPost.objects.hint('tags')
|
||||
self.assertRaises(TypeError, invalid_index)
|
||||
|
||||
def invalid_index_2():
|
||||
return BlogPost.objects.hint(('tags', 1))
|
||||
@@ -567,6 +580,38 @@ class IndexesTest(unittest.TestCase):
|
||||
|
||||
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):
|
||||
"""Ensure that uniqueness constraints are applied to fields on
|
||||
embedded documents. And work with unique_with as well.
|
||||
@@ -753,6 +798,33 @@ class IndexesTest(unittest.TestCase):
|
||||
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__':
|
||||
unittest.main()
|
||||
|
@@ -163,7 +163,7 @@ class InheritanceTest(unittest.TestCase):
|
||||
class Employee(Person):
|
||||
salary = IntField()
|
||||
|
||||
self.assertEqual(['age', 'id', 'name', 'salary'],
|
||||
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
|
||||
sorted(Employee._fields.keys()))
|
||||
self.assertEqual(Employee._get_collection_name(),
|
||||
Person._get_collection_name())
|
||||
@@ -180,7 +180,7 @@ class InheritanceTest(unittest.TestCase):
|
||||
class Employee(Person):
|
||||
salary = IntField()
|
||||
|
||||
self.assertEqual(['age', 'id', 'name', 'salary'],
|
||||
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
|
||||
sorted(Employee._fields.keys()))
|
||||
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
|
||||
['_cls', 'name', 'age'])
|
||||
@@ -397,6 +397,16 @@ class InheritanceTest(unittest.TestCase):
|
||||
meta = {'abstract': True}
|
||||
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):
|
||||
"""Ensure that subclassed documents don't override parents'
|
||||
collections
|
||||
|
@@ -9,7 +9,7 @@ import unittest
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from bson import DBRef
|
||||
from bson import DBRef, ObjectId
|
||||
from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest,
|
||||
PickleDyanmicEmbedded, PickleDynamicTest)
|
||||
|
||||
@@ -34,15 +34,21 @@ class InstanceTest(unittest.TestCase):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
class Job(EmbeddedDocument):
|
||||
name = StringField()
|
||||
years = IntField()
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
job = EmbeddedDocumentField(Job)
|
||||
|
||||
non_field = True
|
||||
|
||||
meta = {"allow_inheritance": True}
|
||||
|
||||
self.Person = Person
|
||||
self.Job = Job
|
||||
|
||||
def tearDown(self):
|
||||
for collection in self.db.collection_names():
|
||||
@@ -50,6 +56,11 @@ class InstanceTest(unittest.TestCase):
|
||||
continue
|
||||
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):
|
||||
"""Ensure that capped collections work properly.
|
||||
"""
|
||||
@@ -103,6 +114,19 @@ class InstanceTest(unittest.TestCase):
|
||||
|
||||
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):
|
||||
self.Person.drop_collection()
|
||||
|
||||
@@ -122,10 +146,18 @@ class InstanceTest(unittest.TestCase):
|
||||
"""
|
||||
class Animal(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
class Fish(Animal): pass
|
||||
class Mammal(Animal): pass
|
||||
class Dog(Mammal): pass
|
||||
class Human(Mammal): pass
|
||||
|
||||
class Fish(Animal):
|
||||
pass
|
||||
|
||||
class Mammal(Animal):
|
||||
pass
|
||||
|
||||
class Dog(Mammal):
|
||||
pass
|
||||
|
||||
class Human(Mammal):
|
||||
pass
|
||||
|
||||
class Zoo(Document):
|
||||
animals = ListField(ReferenceField(Animal))
|
||||
@@ -437,7 +469,7 @@ class InstanceTest(unittest.TestCase):
|
||||
f.reload()
|
||||
except Foo.DoesNotExist:
|
||||
pass
|
||||
except Exception as ex:
|
||||
except Exception:
|
||||
self.assertFalse("Threw wrong exception")
|
||||
|
||||
f.save()
|
||||
@@ -446,13 +478,13 @@ class InstanceTest(unittest.TestCase):
|
||||
f.reload()
|
||||
except Foo.DoesNotExist:
|
||||
pass
|
||||
except Exception as ex:
|
||||
except Exception:
|
||||
self.assertFalse("Threw wrong exception")
|
||||
|
||||
def test_dictionary_access(self):
|
||||
"""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.assertRaises(KeyError, person.__getitem__, 'salary')
|
||||
@@ -462,7 +494,7 @@ class InstanceTest(unittest.TestCase):
|
||||
self.assertEqual(person['name'], 'Another User')
|
||||
|
||||
# Length = length(assigned fields + id)
|
||||
self.assertEqual(len(person), 3)
|
||||
self.assertEqual(len(person), 5)
|
||||
|
||||
self.assertTrue('age' in person)
|
||||
person.age = None
|
||||
@@ -481,8 +513,9 @@ class InstanceTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
|
||||
['_cls', 'name', 'age'])
|
||||
self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
|
||||
['_cls', 'name', 'age', 'salary'])
|
||||
self.assertEqual(
|
||||
Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
|
||||
['_cls', 'name', 'age', 'salary'])
|
||||
|
||||
def test_embedded_document_to_mongo_id(self):
|
||||
class SubDoc(EmbeddedDocument):
|
||||
@@ -617,6 +650,64 @@ class InstanceTest(unittest.TestCase):
|
||||
t = TestDocument(doc=TestEmbeddedDocument(x=15, y=35, z=5))
|
||||
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):
|
||||
"""Ensure that a document may be saved in the database.
|
||||
"""
|
||||
@@ -855,7 +946,7 @@ class InstanceTest(unittest.TestCase):
|
||||
w1 = Widget(toggle=False, save_id=UUID(1))
|
||||
|
||||
# ignore save_condition on new record creation
|
||||
w1.save(save_condition={'save_id':UUID(42)})
|
||||
w1.save(save_condition={'save_id': UUID(42)})
|
||||
w1.reload()
|
||||
self.assertFalse(w1.toggle)
|
||||
self.assertEqual(w1.save_id, UUID(1))
|
||||
@@ -865,7 +956,7 @@ class InstanceTest(unittest.TestCase):
|
||||
flip(w1)
|
||||
self.assertTrue(w1.toggle)
|
||||
self.assertEqual(w1.count, 1)
|
||||
w1.save(save_condition={'save_id':UUID(42)})
|
||||
w1.save(save_condition={'save_id': UUID(42)})
|
||||
w1.reload()
|
||||
self.assertFalse(w1.toggle)
|
||||
self.assertEqual(w1.count, 0)
|
||||
@@ -874,7 +965,7 @@ class InstanceTest(unittest.TestCase):
|
||||
flip(w1)
|
||||
self.assertTrue(w1.toggle)
|
||||
self.assertEqual(w1.count, 1)
|
||||
w1.save(save_condition={'save_id':UUID(1)})
|
||||
w1.save(save_condition={'save_id': UUID(1)})
|
||||
w1.reload()
|
||||
self.assertTrue(w1.toggle)
|
||||
self.assertEqual(w1.count, 1)
|
||||
@@ -887,25 +978,25 @@ class InstanceTest(unittest.TestCase):
|
||||
|
||||
flip(w1)
|
||||
w1.save_id = UUID(2)
|
||||
w1.save(save_condition={'save_id':old_id})
|
||||
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.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.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.save(save_condition={'count__gte': w1.count})
|
||||
w1.reload()
|
||||
self.assertTrue(w1.toggle)
|
||||
self.assertEqual(w1.count, 3)
|
||||
@@ -1351,7 +1442,8 @@ class InstanceTest(unittest.TestCase):
|
||||
self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4')
|
||||
|
||||
def test_save_custom_pk(self):
|
||||
"""Ensure that a document may be saved with a custom _id using pk alias.
|
||||
"""
|
||||
Ensure that a document may be saved with a custom _id using pk alias.
|
||||
"""
|
||||
# Create person object and save it to the database
|
||||
person = self.Person(name='Test User', age=30,
|
||||
@@ -1437,9 +1529,15 @@ class InstanceTest(unittest.TestCase):
|
||||
p4 = Page(comments=[Comment(user=u2, comment="Heavy Metal song")])
|
||||
p4.save()
|
||||
|
||||
self.assertEqual([p1, p2], 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)))
|
||||
self.assertEqual(
|
||||
[p1, p2],
|
||||
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):
|
||||
"""Ensure that a document with an embedded document field may be
|
||||
@@ -1514,7 +1612,8 @@ class InstanceTest(unittest.TestCase):
|
||||
self.assertEqual(promoted_employee.age, 50)
|
||||
|
||||
# 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
|
||||
promoted_employee.details = None
|
||||
@@ -1650,7 +1749,8 @@ class InstanceTest(unittest.TestCase):
|
||||
post.save()
|
||||
|
||||
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)
|
||||
|
||||
# Delete the Person, which should lead to deletion of the BlogPost, too
|
||||
@@ -1699,8 +1799,10 @@ class InstanceTest(unittest.TestCase):
|
||||
|
||||
class BlogPost(Document):
|
||||
content = StringField()
|
||||
authors = ListField(ReferenceField(self.Person, reverse_delete_rule=CASCADE))
|
||||
reviewers = ListField(ReferenceField(self.Person, reverse_delete_rule=NULLIFY))
|
||||
authors = ListField(ReferenceField(
|
||||
self.Person, reverse_delete_rule=CASCADE))
|
||||
reviewers = ListField(ReferenceField(
|
||||
self.Person, reverse_delete_rule=NULLIFY))
|
||||
|
||||
self.Person.drop_collection()
|
||||
|
||||
@@ -1795,13 +1897,17 @@ class InstanceTest(unittest.TestCase):
|
||||
self.assertEqual(Bar.objects.count(), 1) # No effect on the BlogPost
|
||||
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():
|
||||
class Blog(Document):
|
||||
content = StringField()
|
||||
authors = MapField(ReferenceField(self.Person, reverse_delete_rule=CASCADE))
|
||||
reviewers = DictField(field=ReferenceField(self.Person, reverse_delete_rule=NULLIFY))
|
||||
authors = MapField(ReferenceField(
|
||||
self.Person, reverse_delete_rule=CASCADE))
|
||||
reviewers = DictField(
|
||||
field=ReferenceField(
|
||||
self.Person,
|
||||
reverse_delete_rule=NULLIFY))
|
||||
|
||||
self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
|
||||
|
||||
@@ -1810,7 +1916,8 @@ class InstanceTest(unittest.TestCase):
|
||||
father = 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):
|
||||
"""Ensure that a chain of documents is also deleted upon cascaded
|
||||
@@ -1832,16 +1939,16 @@ class InstanceTest(unittest.TestCase):
|
||||
author = self.Person(name='Test User')
|
||||
author.save()
|
||||
|
||||
post = BlogPost(content = 'Watched some TV')
|
||||
post = BlogPost(content='Watched some TV')
|
||||
post.author = author
|
||||
post.save()
|
||||
|
||||
comment = Comment(text = 'Kudos.')
|
||||
comment = Comment(text='Kudos.')
|
||||
comment.post = post
|
||||
comment.save()
|
||||
|
||||
# Delete the Person, which should lead to deletion of the BlogPost, and,
|
||||
# recursively to the Comment, too
|
||||
# Delete the Person, which should lead to deletion of the BlogPost,
|
||||
# and, recursively to the Comment, too
|
||||
author.delete()
|
||||
self.assertEqual(Comment.objects.count(), 0)
|
||||
|
||||
@@ -1864,7 +1971,7 @@ class InstanceTest(unittest.TestCase):
|
||||
author = self.Person(name='Test User')
|
||||
author.save()
|
||||
|
||||
post = BlogPost(content = 'Watched some TV')
|
||||
post = BlogPost(content='Watched some TV')
|
||||
post.author = author
|
||||
post.save()
|
||||
|
||||
@@ -1980,7 +2087,8 @@ class InstanceTest(unittest.TestCase):
|
||||
|
||||
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")
|
||||
pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved
|
||||
|
||||
@@ -2002,7 +2110,8 @@ class InstanceTest(unittest.TestCase):
|
||||
pickle_doc.embedded._dynamic_fields.keys())
|
||||
|
||||
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.save()
|
||||
pickle_doc.delete()
|
||||
@@ -2157,9 +2266,15 @@ class InstanceTest(unittest.TestCase):
|
||||
self.assertEqual(AuthorBooks._get_db(), get_db("testdb-3"))
|
||||
|
||||
# Collections
|
||||
self.assertEqual(User._get_collection(), 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()])
|
||||
self.assertEqual(
|
||||
User._get_collection(),
|
||||
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):
|
||||
"""db_alias can be overriden
|
||||
@@ -2328,30 +2443,6 @@ class InstanceTest(unittest.TestCase):
|
||||
group = Group.objects.first()
|
||||
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):
|
||||
|
||||
class Embedded(DynamicEmbeddedDocument):
|
||||
@@ -2427,6 +2518,10 @@ class InstanceTest(unittest.TestCase):
|
||||
doc_name = StringField()
|
||||
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"))
|
||||
dict_doc = Doc(**{"doc_name": "my doc",
|
||||
"doc": {"name": "embedded doc"}})
|
||||
@@ -2443,6 +2538,10 @@ class InstanceTest(unittest.TestCase):
|
||||
doc_name = StringField()
|
||||
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=[
|
||||
Embedded(name="embedded doc1"),
|
||||
Embedded(name="embedded doc2")])
|
||||
@@ -2537,7 +2636,9 @@ class InstanceTest(unittest.TestCase):
|
||||
system.save()
|
||||
|
||||
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):
|
||||
|
||||
@@ -2643,5 +2744,60 @@ class InstanceTest(unittest.TestCase):
|
||||
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__':
|
||||
unittest.main()
|
||||
|
@@ -51,6 +51,10 @@ class TestJson(unittest.TestCase):
|
||||
string = StringField()
|
||||
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_json = doc.to_json(sort_keys=True, separators=(',', ':'))
|
||||
@@ -99,6 +103,10 @@ class TestJson(unittest.TestCase):
|
||||
generic_embedded_document_field = GenericEmbeddedDocumentField(
|
||||
default=lambda: EmbeddedDoc())
|
||||
|
||||
def __eq__(self, other):
|
||||
import json
|
||||
return json.loads(self.to_json()) == json.loads(other.to_json())
|
||||
|
||||
doc = Doc()
|
||||
self.assertEqual(doc, Doc.from_json(doc.to_json()))
|
||||
|
||||
|
@@ -18,10 +18,11 @@ from bson import Binary, DBRef, ObjectId
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.base import _document_registry
|
||||
from mongoengine.base.datastructures import BaseDict, EmbeddedDocumentList
|
||||
from mongoengine.errors import NotRegistered
|
||||
from mongoengine.python_support import PY3, b, bin_type
|
||||
|
||||
__all__ = ("FieldTest", )
|
||||
__all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase")
|
||||
|
||||
|
||||
class FieldTest(unittest.TestCase):
|
||||
@@ -1175,6 +1176,11 @@ class FieldTest(unittest.TestCase):
|
||||
post.reload()
|
||||
self.assertEqual('updated', post.info['title'])
|
||||
|
||||
post.info.setdefault('authors', [])
|
||||
post.save()
|
||||
post.reload()
|
||||
self.assertEqual([], post.info['authors'])
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_dictfield_strict(self):
|
||||
@@ -1251,6 +1257,30 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
Simple.drop_collection()
|
||||
|
||||
def test_atomic_update_dict_field(self):
|
||||
"""Ensure that the entire DictField can be atomically updated."""
|
||||
|
||||
|
||||
class Simple(Document):
|
||||
mapping = DictField(field=ListField(IntField(required=True)))
|
||||
|
||||
Simple.drop_collection()
|
||||
|
||||
e = Simple()
|
||||
e.mapping['someints'] = [1, 2]
|
||||
e.save()
|
||||
e.update(set__mapping={"ints": [3, 4]})
|
||||
e.reload()
|
||||
self.assertEqual(BaseDict, type(e.mapping))
|
||||
self.assertEqual({"ints": [3, 4]}, e.mapping)
|
||||
|
||||
def create_invalid_mapping():
|
||||
e.update(set__mapping={"somestrings": ["foo", "bar",]})
|
||||
|
||||
self.assertRaises(ValueError, create_invalid_mapping)
|
||||
|
||||
Simple.drop_collection()
|
||||
|
||||
def test_mapfield(self):
|
||||
"""Ensure that the MapField handles the declared type."""
|
||||
|
||||
@@ -1789,7 +1819,7 @@ class FieldTest(unittest.TestCase):
|
||||
Animal.drop_collection()
|
||||
Ocorrence.drop_collection()
|
||||
|
||||
a = Animal(nam="Leopard", tag="heavy",
|
||||
a = Animal(name="Leopard", tag="heavy",
|
||||
owner=Owner(tp='u', name="Wilson Júnior")
|
||||
)
|
||||
a.save()
|
||||
@@ -1839,7 +1869,7 @@ class FieldTest(unittest.TestCase):
|
||||
Animal.drop_collection()
|
||||
Ocorrence.drop_collection()
|
||||
|
||||
a = Animal(nam="Leopard", tag="heavy",
|
||||
a = Animal(name="Leopard", tag="heavy",
|
||||
owner=Owner(tags=['cool', 'funny'],
|
||||
name="Wilson Júnior")
|
||||
)
|
||||
@@ -1949,14 +1979,14 @@ class FieldTest(unittest.TestCase):
|
||||
def test_recursive_embedding(self):
|
||||
"""Ensure that EmbeddedDocumentFields can contain their own documents.
|
||||
"""
|
||||
class Tree(Document):
|
||||
name = StringField()
|
||||
children = ListField(EmbeddedDocumentField('TreeNode'))
|
||||
|
||||
class TreeNode(EmbeddedDocument):
|
||||
name = StringField()
|
||||
children = ListField(EmbeddedDocumentField('self'))
|
||||
|
||||
class Tree(Document):
|
||||
name = StringField()
|
||||
children = ListField(EmbeddedDocumentField('TreeNode'))
|
||||
|
||||
Tree.drop_collection()
|
||||
tree = Tree(name="Tree")
|
||||
|
||||
@@ -2421,6 +2451,79 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
Shirt.drop_collection()
|
||||
|
||||
def test_choices_validation_documents(self):
|
||||
"""
|
||||
Ensure fields with document choices validate given a valid choice.
|
||||
"""
|
||||
class UserComments(EmbeddedDocument):
|
||||
author = StringField()
|
||||
message = StringField()
|
||||
|
||||
class BlogPost(Document):
|
||||
comments = ListField(
|
||||
GenericEmbeddedDocumentField(choices=(UserComments,))
|
||||
)
|
||||
|
||||
# Ensure Validation Passes
|
||||
BlogPost(comments=[
|
||||
UserComments(author='user2', message='message2'),
|
||||
]).save()
|
||||
|
||||
def test_choices_validation_documents_invalid(self):
|
||||
"""
|
||||
Ensure fields with document choices validate given an invalid choice.
|
||||
This should throw a ValidationError exception.
|
||||
"""
|
||||
class UserComments(EmbeddedDocument):
|
||||
author = StringField()
|
||||
message = StringField()
|
||||
|
||||
class ModeratorComments(EmbeddedDocument):
|
||||
author = StringField()
|
||||
message = StringField()
|
||||
|
||||
class BlogPost(Document):
|
||||
comments = ListField(
|
||||
GenericEmbeddedDocumentField(choices=(UserComments,))
|
||||
)
|
||||
|
||||
# Single Entry Failure
|
||||
post = BlogPost(comments=[
|
||||
ModeratorComments(author='mod1', message='message1'),
|
||||
])
|
||||
self.assertRaises(ValidationError, post.save)
|
||||
|
||||
# Mixed Entry Failure
|
||||
post = BlogPost(comments=[
|
||||
ModeratorComments(author='mod1', message='message1'),
|
||||
UserComments(author='user2', message='message2'),
|
||||
])
|
||||
self.assertRaises(ValidationError, post.save)
|
||||
|
||||
def test_choices_validation_documents_inheritance(self):
|
||||
"""
|
||||
Ensure fields with document choices validate given subclass of choice.
|
||||
"""
|
||||
class Comments(EmbeddedDocument):
|
||||
meta = {
|
||||
'abstract': True
|
||||
}
|
||||
author = StringField()
|
||||
message = StringField()
|
||||
|
||||
class UserComments(Comments):
|
||||
pass
|
||||
|
||||
class BlogPost(Document):
|
||||
comments = ListField(
|
||||
GenericEmbeddedDocumentField(choices=(Comments,))
|
||||
)
|
||||
|
||||
# Save Valid EmbeddedDocument Type
|
||||
BlogPost(comments=[
|
||||
UserComments(author='user2', message='message2'),
|
||||
]).save()
|
||||
|
||||
def test_choices_get_field_display(self):
|
||||
"""Test dynamic helper for returning the display value of a choices
|
||||
field.
|
||||
@@ -2692,9 +2795,11 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
class Animal(Document):
|
||||
id = SequenceField(primary_key=True)
|
||||
name = StringField()
|
||||
|
||||
class Person(Document):
|
||||
id = SequenceField(primary_key=True)
|
||||
name = StringField()
|
||||
|
||||
self.db['mongoengine.counters'].drop()
|
||||
Animal.drop_collection()
|
||||
@@ -2902,6 +3007,9 @@ class FieldTest(unittest.TestCase):
|
||||
"aJIazqqWkm7.net"))
|
||||
self.assertTrue(user.validate() is None)
|
||||
|
||||
user = User(email="new-tld@example.technology")
|
||||
self.assertTrue(user.validate() is None)
|
||||
|
||||
user = User(email='me@localhost')
|
||||
self.assertRaises(ValidationError, user.validate)
|
||||
|
||||
@@ -3006,6 +3114,518 @@ class FieldTest(unittest.TestCase):
|
||||
test.dictionary # Just access to test getter
|
||||
self.assertRaises(ValidationError, test.validate)
|
||||
|
||||
def test_cls_field(self):
|
||||
class Animal(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class Fish(Animal):
|
||||
pass
|
||||
|
||||
class Mammal(Animal):
|
||||
pass
|
||||
|
||||
class Dog(Mammal):
|
||||
pass
|
||||
|
||||
class Human(Mammal):
|
||||
pass
|
||||
|
||||
Animal.objects.delete()
|
||||
Dog().save()
|
||||
Fish().save()
|
||||
Human().save()
|
||||
self.assertEquals(Animal.objects(_cls__in=["Animal.Mammal.Dog", "Animal.Fish"]).count(), 2)
|
||||
self.assertEquals(Animal.objects(_cls__in=["Animal.Fish.Guppy"]).count(), 0)
|
||||
|
||||
def test_sparse_field(self):
|
||||
class Doc(Document):
|
||||
name = StringField(required=False, unique=True, sparse=True)
|
||||
try:
|
||||
Doc().save()
|
||||
Doc().save()
|
||||
except Exception:
|
||||
self.fail()
|
||||
|
||||
def test_undefined_field_exception(self):
|
||||
"""Tests if a `FieldDoesNotExist` exception is raised when trying to
|
||||
set a value to a field that's not defined.
|
||||
"""
|
||||
|
||||
class Doc(Document):
|
||||
foo = StringField(db_field='f')
|
||||
|
||||
def test():
|
||||
Doc(bar='test')
|
||||
|
||||
self.assertRaises(FieldDoesNotExist, test)
|
||||
|
||||
|
||||
class EmbeddedDocumentListFieldTestCase(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.db = connect(db='EmbeddedDocumentListFieldTestCase')
|
||||
|
||||
class Comments(EmbeddedDocument):
|
||||
author = StringField()
|
||||
message = StringField()
|
||||
|
||||
class BlogPost(Document):
|
||||
comments = EmbeddedDocumentListField(Comments)
|
||||
|
||||
cls.Comments = Comments
|
||||
cls.BlogPost = BlogPost
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create two BlogPost entries in the database, each with
|
||||
several EmbeddedDocuments.
|
||||
"""
|
||||
self.post1 = self.BlogPost(comments=[
|
||||
self.Comments(author='user1', message='message1'),
|
||||
self.Comments(author='user2', message='message1')
|
||||
]).save()
|
||||
|
||||
self.post2 = self.BlogPost(comments=[
|
||||
self.Comments(author='user2', message='message2'),
|
||||
self.Comments(author='user2', message='message3'),
|
||||
self.Comments(author='user3', message='message1')
|
||||
]).save()
|
||||
|
||||
def tearDown(self):
|
||||
self.BlogPost.drop_collection()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.db.drop_database('EmbeddedDocumentListFieldTestCase')
|
||||
|
||||
def test_no_keyword_filter(self):
|
||||
"""
|
||||
Tests the filter method of a List of Embedded Documents
|
||||
with a no keyword.
|
||||
"""
|
||||
filtered = self.post1.comments.filter()
|
||||
|
||||
# Ensure nothing was changed
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertListEqual(filtered, self.post1.comments)
|
||||
self.assertEqual(filtered, self.post1.comments)
|
||||
|
||||
def test_single_keyword_filter(self):
|
||||
"""
|
||||
Tests the filter method of a List of Embedded Documents
|
||||
with a single keyword.
|
||||
"""
|
||||
filtered = self.post1.comments.filter(author='user1')
|
||||
|
||||
# Ensure only 1 entry was returned.
|
||||
self.assertEqual(len(filtered), 1)
|
||||
|
||||
# Ensure the entry returned is the correct entry.
|
||||
self.assertEqual(filtered[0].author, 'user1')
|
||||
|
||||
def test_multi_keyword_filter(self):
|
||||
"""
|
||||
Tests the filter method of a List of Embedded Documents
|
||||
with multiple keywords.
|
||||
"""
|
||||
filtered = self.post2.comments.filter(
|
||||
author='user2', message='message2'
|
||||
)
|
||||
|
||||
# Ensure only 1 entry was returned.
|
||||
self.assertEqual(len(filtered), 1)
|
||||
|
||||
# Ensure the entry returned is the correct entry.
|
||||
self.assertEqual(filtered[0].author, 'user2')
|
||||
self.assertEqual(filtered[0].message, 'message2')
|
||||
|
||||
def test_chained_filter(self):
|
||||
"""
|
||||
Tests chained filter methods of a List of Embedded Documents
|
||||
"""
|
||||
filtered = self.post2.comments.filter(author='user2').filter(
|
||||
message='message2'
|
||||
)
|
||||
|
||||
# Ensure only 1 entry was returned.
|
||||
self.assertEqual(len(filtered), 1)
|
||||
|
||||
# Ensure the entry returned is the correct entry.
|
||||
self.assertEqual(filtered[0].author, 'user2')
|
||||
self.assertEqual(filtered[0].message, 'message2')
|
||||
|
||||
def test_unknown_keyword_filter(self):
|
||||
"""
|
||||
Tests the filter method of a List of Embedded Documents
|
||||
when the keyword is not a known keyword.
|
||||
"""
|
||||
# < 2.6 Incompatible >
|
||||
# with self.assertRaises(AttributeError):
|
||||
# self.post2.comments.filter(year=2)
|
||||
self.assertRaises(AttributeError, self.post2.comments.filter, year=2)
|
||||
|
||||
def test_no_keyword_exclude(self):
|
||||
"""
|
||||
Tests the exclude method of a List of Embedded Documents
|
||||
with a no keyword.
|
||||
"""
|
||||
filtered = self.post1.comments.exclude()
|
||||
|
||||
# Ensure everything was removed
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertListEqual(filtered, [])
|
||||
self.assertEqual(filtered, [])
|
||||
|
||||
def test_single_keyword_exclude(self):
|
||||
"""
|
||||
Tests the exclude method of a List of Embedded Documents
|
||||
with a single keyword.
|
||||
"""
|
||||
excluded = self.post1.comments.exclude(author='user1')
|
||||
|
||||
# Ensure only 1 entry was returned.
|
||||
self.assertEqual(len(excluded), 1)
|
||||
|
||||
# Ensure the entry returned is the correct entry.
|
||||
self.assertEqual(excluded[0].author, 'user2')
|
||||
|
||||
def test_multi_keyword_exclude(self):
|
||||
"""
|
||||
Tests the exclude method of a List of Embedded Documents
|
||||
with multiple keywords.
|
||||
"""
|
||||
excluded = self.post2.comments.exclude(
|
||||
author='user3', message='message1'
|
||||
)
|
||||
|
||||
# Ensure only 2 entries were returned.
|
||||
self.assertEqual(len(excluded), 2)
|
||||
|
||||
# Ensure the entries returned are the correct entries.
|
||||
self.assertEqual(excluded[0].author, 'user2')
|
||||
self.assertEqual(excluded[1].author, 'user2')
|
||||
|
||||
def test_non_matching_exclude(self):
|
||||
"""
|
||||
Tests the exclude method of a List of Embedded Documents
|
||||
when the keyword does not match any entries.
|
||||
"""
|
||||
excluded = self.post2.comments.exclude(author='user4')
|
||||
|
||||
# Ensure the 3 entries still exist.
|
||||
self.assertEqual(len(excluded), 3)
|
||||
|
||||
def test_unknown_keyword_exclude(self):
|
||||
"""
|
||||
Tests the exclude method of a List of Embedded Documents
|
||||
when the keyword is not a known keyword.
|
||||
"""
|
||||
# < 2.6 Incompatible >
|
||||
# with self.assertRaises(AttributeError):
|
||||
# self.post2.comments.exclude(year=2)
|
||||
self.assertRaises(AttributeError, self.post2.comments.exclude, year=2)
|
||||
|
||||
def test_chained_filter_exclude(self):
|
||||
"""
|
||||
Tests the exclude method after a filter method of a List of
|
||||
Embedded Documents.
|
||||
"""
|
||||
excluded = self.post2.comments.filter(author='user2').exclude(
|
||||
message='message2'
|
||||
)
|
||||
|
||||
# Ensure only 1 entry was returned.
|
||||
self.assertEqual(len(excluded), 1)
|
||||
|
||||
# Ensure the entry returned is the correct entry.
|
||||
self.assertEqual(excluded[0].author, 'user2')
|
||||
self.assertEqual(excluded[0].message, 'message3')
|
||||
|
||||
def test_count(self):
|
||||
"""
|
||||
Tests the count method of a List of Embedded Documents.
|
||||
"""
|
||||
self.assertEqual(self.post1.comments.count(), 2)
|
||||
self.assertEqual(self.post1.comments.count(), len(self.post1.comments))
|
||||
|
||||
def test_filtered_count(self):
|
||||
"""
|
||||
Tests the filter + count method of a List of Embedded Documents.
|
||||
"""
|
||||
count = self.post1.comments.filter(author='user1').count()
|
||||
self.assertEqual(count, 1)
|
||||
|
||||
def test_single_keyword_get(self):
|
||||
"""
|
||||
Tests the get method of a List of Embedded Documents using a
|
||||
single keyword.
|
||||
"""
|
||||
comment = self.post1.comments.get(author='user1')
|
||||
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIsInstance(comment, self.Comments)
|
||||
self.assertTrue(isinstance(comment, self.Comments))
|
||||
self.assertEqual(comment.author, 'user1')
|
||||
|
||||
def test_multi_keyword_get(self):
|
||||
"""
|
||||
Tests the get method of a List of Embedded Documents using
|
||||
multiple keywords.
|
||||
"""
|
||||
comment = self.post2.comments.get(author='user2', message='message2')
|
||||
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIsInstance(comment, self.Comments)
|
||||
self.assertTrue(isinstance(comment, self.Comments))
|
||||
self.assertEqual(comment.author, 'user2')
|
||||
self.assertEqual(comment.message, 'message2')
|
||||
|
||||
def test_no_keyword_multiple_return_get(self):
|
||||
"""
|
||||
Tests the get method of a List of Embedded Documents without
|
||||
a keyword to return multiple documents.
|
||||
"""
|
||||
# < 2.6 Incompatible >
|
||||
# with self.assertRaises(MultipleObjectsReturned):
|
||||
# self.post1.comments.get()
|
||||
self.assertRaises(MultipleObjectsReturned, self.post1.comments.get)
|
||||
|
||||
def test_keyword_multiple_return_get(self):
|
||||
"""
|
||||
Tests the get method of a List of Embedded Documents with a keyword
|
||||
to return multiple documents.
|
||||
"""
|
||||
# < 2.6 Incompatible >
|
||||
# with self.assertRaises(MultipleObjectsReturned):
|
||||
# self.post2.comments.get(author='user2')
|
||||
self.assertRaises(
|
||||
MultipleObjectsReturned, self.post2.comments.get, author='user2'
|
||||
)
|
||||
|
||||
def test_unknown_keyword_get(self):
|
||||
"""
|
||||
Tests the get method of a List of Embedded Documents with an
|
||||
unknown keyword.
|
||||
"""
|
||||
# < 2.6 Incompatible >
|
||||
# with self.assertRaises(AttributeError):
|
||||
# self.post2.comments.get(year=2020)
|
||||
self.assertRaises(AttributeError, self.post2.comments.get, year=2020)
|
||||
|
||||
def test_no_result_get(self):
|
||||
"""
|
||||
Tests the get method of a List of Embedded Documents where get
|
||||
returns no results.
|
||||
"""
|
||||
# < 2.6 Incompatible >
|
||||
# with self.assertRaises(DoesNotExist):
|
||||
# self.post1.comments.get(author='user3')
|
||||
self.assertRaises(
|
||||
DoesNotExist, self.post1.comments.get, author='user3'
|
||||
)
|
||||
|
||||
def test_first(self):
|
||||
"""
|
||||
Tests the first method of a List of Embedded Documents to
|
||||
ensure it returns the first comment.
|
||||
"""
|
||||
comment = self.post1.comments.first()
|
||||
|
||||
# Ensure a Comment object was returned.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIsInstance(comment, self.Comments)
|
||||
self.assertTrue(isinstance(comment, self.Comments))
|
||||
self.assertEqual(comment, self.post1.comments[0])
|
||||
|
||||
def test_create(self):
|
||||
"""
|
||||
Test the create method of a List of Embedded Documents.
|
||||
"""
|
||||
comment = self.post1.comments.create(
|
||||
author='user4', message='message1'
|
||||
)
|
||||
self.post1.save()
|
||||
|
||||
# Ensure the returned value is the comment object.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIsInstance(comment, self.Comments)
|
||||
self.assertTrue(isinstance(comment, self.Comments))
|
||||
self.assertEqual(comment.author, 'user4')
|
||||
self.assertEqual(comment.message, 'message1')
|
||||
|
||||
# Ensure the new comment was actually saved to the database.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIn(
|
||||
# comment,
|
||||
# self.BlogPost.objects(comments__author='user4')[0].comments
|
||||
# )
|
||||
self.assertTrue(
|
||||
comment in self.BlogPost.objects(
|
||||
comments__author='user4'
|
||||
)[0].comments
|
||||
)
|
||||
|
||||
def test_filtered_create(self):
|
||||
"""
|
||||
Test the create method of a List of Embedded Documents chained
|
||||
to a call to the filter method. Filtering should have no effect
|
||||
on creation.
|
||||
"""
|
||||
comment = self.post1.comments.filter(author='user1').create(
|
||||
author='user4', message='message1'
|
||||
)
|
||||
self.post1.save()
|
||||
|
||||
# Ensure the returned value is the comment object.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIsInstance(comment, self.Comments)
|
||||
self.assertTrue(isinstance(comment, self.Comments))
|
||||
self.assertEqual(comment.author, 'user4')
|
||||
self.assertEqual(comment.message, 'message1')
|
||||
|
||||
# Ensure the new comment was actually saved to the database.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIn(
|
||||
# comment,
|
||||
# self.BlogPost.objects(comments__author='user4')[0].comments
|
||||
# )
|
||||
self.assertTrue(
|
||||
comment in self.BlogPost.objects(
|
||||
comments__author='user4'
|
||||
)[0].comments
|
||||
)
|
||||
|
||||
def test_no_keyword_update(self):
|
||||
"""
|
||||
Tests the update method of a List of Embedded Documents with
|
||||
no keywords.
|
||||
"""
|
||||
original = list(self.post1.comments)
|
||||
number = self.post1.comments.update()
|
||||
self.post1.save()
|
||||
|
||||
# Ensure that nothing was altered.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIn(
|
||||
# original[0],
|
||||
# self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
# )
|
||||
self.assertTrue(
|
||||
original[0] in self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
)
|
||||
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIn(
|
||||
# original[1],
|
||||
# self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
# )
|
||||
self.assertTrue(
|
||||
original[1] in self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
)
|
||||
|
||||
# Ensure the method returned 0 as the number of entries
|
||||
# modified
|
||||
self.assertEqual(number, 0)
|
||||
|
||||
def test_single_keyword_update(self):
|
||||
"""
|
||||
Tests the update method of a List of Embedded Documents with
|
||||
a single keyword.
|
||||
"""
|
||||
number = self.post1.comments.update(author='user4')
|
||||
self.post1.save()
|
||||
|
||||
comments = self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
|
||||
# Ensure that the database was updated properly.
|
||||
self.assertEqual(comments[0].author, 'user4')
|
||||
self.assertEqual(comments[1].author, 'user4')
|
||||
|
||||
# Ensure the method returned 2 as the number of entries
|
||||
# modified
|
||||
self.assertEqual(number, 2)
|
||||
|
||||
def test_save(self):
|
||||
"""
|
||||
Tests the save method of a List of Embedded Documents.
|
||||
"""
|
||||
comments = self.post1.comments
|
||||
new_comment = self.Comments(author='user4')
|
||||
comments.append(new_comment)
|
||||
comments.save()
|
||||
|
||||
# Ensure that the new comment has been added to the database.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIn(
|
||||
# new_comment,
|
||||
# self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
# )
|
||||
self.assertTrue(
|
||||
new_comment in self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
)
|
||||
|
||||
def test_delete(self):
|
||||
"""
|
||||
Tests the delete method of a List of Embedded Documents.
|
||||
"""
|
||||
number = self.post1.comments.delete()
|
||||
self.post1.save()
|
||||
|
||||
# Ensure that all the comments under post1 were deleted in the
|
||||
# database.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertListEqual(
|
||||
# self.BlogPost.objects(id=self.post1.id)[0].comments, []
|
||||
# )
|
||||
self.assertEqual(
|
||||
self.BlogPost.objects(id=self.post1.id)[0].comments, []
|
||||
)
|
||||
|
||||
# Ensure that post1 comments were deleted from the list.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertListEqual(self.post1.comments, [])
|
||||
self.assertEqual(self.post1.comments, [])
|
||||
|
||||
# Ensure that comments still returned a EmbeddedDocumentList object.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertIsInstance(self.post1.comments, EmbeddedDocumentList)
|
||||
self.assertTrue(isinstance(self.post1.comments, EmbeddedDocumentList))
|
||||
|
||||
# Ensure that the delete method returned 2 as the number of entries
|
||||
# deleted from the database
|
||||
self.assertEqual(number, 2)
|
||||
|
||||
def test_filtered_delete(self):
|
||||
"""
|
||||
Tests the delete method of a List of Embedded Documents
|
||||
after the filter method has been called.
|
||||
"""
|
||||
comment = self.post1.comments[1]
|
||||
number = self.post1.comments.filter(author='user2').delete()
|
||||
self.post1.save()
|
||||
|
||||
# Ensure that only the user2 comment was deleted.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertNotIn(
|
||||
# comment, self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
# )
|
||||
self.assertTrue(
|
||||
comment not in self.BlogPost.objects(id=self.post1.id)[0].comments
|
||||
)
|
||||
self.assertEqual(
|
||||
len(self.BlogPost.objects(id=self.post1.id)[0].comments), 1
|
||||
)
|
||||
|
||||
# Ensure that the user2 comment no longer exists in the list.
|
||||
# < 2.6 Incompatible >
|
||||
# self.assertNotIn(comment, self.post1.comments)
|
||||
self.assertTrue(comment not in self.post1.comments)
|
||||
self.assertEqual(len(self.post1.comments), 1)
|
||||
|
||||
# Ensure that the delete method returned 1 as the number of entries
|
||||
# deleted from the database
|
||||
self.assertEqual(number, 1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -114,6 +114,42 @@ class FileTest(unittest.TestCase):
|
||||
# Ensure deleted file returns 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):
|
||||
|
||||
class SetFile(Document):
|
||||
|
@@ -19,8 +19,8 @@ class GeoFieldTest(unittest.TestCase):
|
||||
def _test_for_expected_error(self, Cls, loc, expected):
|
||||
try:
|
||||
Cls(loc=loc).validate()
|
||||
self.fail()
|
||||
except ValidationError, e:
|
||||
self.fail('Should not validate the location {0}'.format(loc))
|
||||
except ValidationError as e:
|
||||
self.assertEqual(expected, e.to_dict()['loc'])
|
||||
|
||||
def test_geopoint_validation(self):
|
||||
@@ -155,6 +155,117 @@ class GeoFieldTest(unittest.TestCase):
|
||||
|
||||
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):
|
||||
"""Ensure that indexes are created automatically for GeoPointFields.
|
||||
"""
|
||||
|
@@ -510,18 +510,29 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertEqual(post.comments[0].by, 'joe')
|
||||
self.assertEqual(post.comments[0].votes.score, 4)
|
||||
|
||||
def test_updates_can_have_match_operators(self):
|
||||
def test_update_min_max(self):
|
||||
class Scores(Document):
|
||||
high_score = IntField()
|
||||
low_score = IntField()
|
||||
scores = Scores(high_score=800, low_score=200)
|
||||
scores.save()
|
||||
Scores.objects(id=scores.id).update(min__low_score=150)
|
||||
self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150)
|
||||
Scores.objects(id=scores.id).update(min__low_score=250)
|
||||
self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150)
|
||||
|
||||
class Post(Document):
|
||||
title = StringField(required=True)
|
||||
tags = ListField(StringField())
|
||||
comments = ListField(EmbeddedDocumentField("Comment"))
|
||||
def test_updates_can_have_match_operators(self):
|
||||
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField()
|
||||
name = StringField(max_length=120)
|
||||
vote = IntField()
|
||||
|
||||
class Post(Document):
|
||||
title = StringField(required=True)
|
||||
tags = ListField(StringField())
|
||||
comments = ListField(EmbeddedDocumentField("Comment"))
|
||||
|
||||
Post.drop_collection()
|
||||
|
||||
comm1 = Comment(content="very funny indeed", name="John S", vote=1)
|
||||
@@ -914,7 +925,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
docs = docs[1:4]
|
||||
self.assertEqual('[<Doc: 1>, <Doc: 2>, <Doc: 3>]', "%s" % docs)
|
||||
|
||||
self.assertEqual(docs.count(), 3)
|
||||
self.assertEqual(docs.count(with_limit_and_skip=True), 3)
|
||||
for doc in docs:
|
||||
self.assertEqual('.. queryset mid-iteration ..', repr(docs))
|
||||
|
||||
@@ -1302,6 +1313,31 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.Person.objects(name='Test User').delete()
|
||||
self.assertEqual(1, BlogPost.objects.count())
|
||||
|
||||
def test_reverse_delete_rule_cascade_on_abstract_document(self):
|
||||
"""Ensure cascading deletion of referring documents from the database
|
||||
does not fail on abstract document.
|
||||
"""
|
||||
class AbstractBlogPost(Document):
|
||||
meta = {'abstract': True}
|
||||
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
|
||||
|
||||
class BlogPost(AbstractBlogPost):
|
||||
content = StringField()
|
||||
BlogPost.drop_collection()
|
||||
|
||||
me = self.Person(name='Test User')
|
||||
me.save()
|
||||
someoneelse = self.Person(name='Some-one Else')
|
||||
someoneelse.save()
|
||||
|
||||
BlogPost(content='Watching TV', author=me).save()
|
||||
BlogPost(content='Chilling out', author=me).save()
|
||||
BlogPost(content='Pro Testing', author=someoneelse).save()
|
||||
|
||||
self.assertEqual(3, BlogPost.objects.count())
|
||||
self.Person.objects(name='Test User').delete()
|
||||
self.assertEqual(1, BlogPost.objects.count())
|
||||
|
||||
def test_reverse_delete_rule_cascade_self_referencing(self):
|
||||
"""Ensure self-referencing CASCADE deletes do not result in infinite
|
||||
loop
|
||||
@@ -1361,6 +1397,31 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertEqual(1, BlogPost.objects.count())
|
||||
self.assertEqual(None, BlogPost.objects.first().category)
|
||||
|
||||
def test_reverse_delete_rule_nullify_on_abstract_document(self):
|
||||
"""Ensure nullification of references to deleted documents when
|
||||
reference is on an abstract document.
|
||||
"""
|
||||
class AbstractBlogPost(Document):
|
||||
meta = {'abstract': True}
|
||||
author = ReferenceField(self.Person, reverse_delete_rule=NULLIFY)
|
||||
|
||||
class BlogPost(AbstractBlogPost):
|
||||
content = StringField()
|
||||
BlogPost.drop_collection()
|
||||
|
||||
me = self.Person(name='Test User')
|
||||
me.save()
|
||||
someoneelse = self.Person(name='Some-one Else')
|
||||
someoneelse.save()
|
||||
|
||||
BlogPost(content='Watching TV', author=me).save()
|
||||
|
||||
self.assertEqual(1, BlogPost.objects.count())
|
||||
self.assertEqual(me, BlogPost.objects.first().author)
|
||||
self.Person.objects(name='Test User').delete()
|
||||
self.assertEqual(1, BlogPost.objects.count())
|
||||
self.assertEqual(None, BlogPost.objects.first().author)
|
||||
|
||||
def test_reverse_delete_rule_deny(self):
|
||||
"""Ensure deletion gets denied on documents that still have references
|
||||
to them.
|
||||
@@ -1380,6 +1441,26 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
self.assertRaises(OperationError, self.Person.objects.delete)
|
||||
|
||||
def test_reverse_delete_rule_deny_on_abstract_document(self):
|
||||
"""Ensure deletion gets denied on documents that still have references
|
||||
to them, when reference is on an abstract document.
|
||||
"""
|
||||
class AbstractBlogPost(Document):
|
||||
meta = {'abstract': True}
|
||||
author = ReferenceField(self.Person, reverse_delete_rule=DENY)
|
||||
|
||||
class BlogPost(AbstractBlogPost):
|
||||
content = StringField()
|
||||
BlogPost.drop_collection()
|
||||
|
||||
me = self.Person(name='Test User')
|
||||
me.save()
|
||||
|
||||
BlogPost(content='Watching TV', author=me).save()
|
||||
|
||||
self.assertEqual(1, BlogPost.objects.count())
|
||||
self.assertRaises(OperationError, self.Person.objects.delete)
|
||||
|
||||
def test_reverse_delete_rule_pull(self):
|
||||
"""Ensure pulling of references to deleted documents.
|
||||
"""
|
||||
@@ -1410,6 +1491,40 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertEqual(post.authors, [me])
|
||||
self.assertEqual(another.authors, [])
|
||||
|
||||
def test_reverse_delete_rule_pull_on_abstract_documents(self):
|
||||
"""Ensure pulling of references to deleted documents when reference
|
||||
is defined on an abstract document..
|
||||
"""
|
||||
class AbstractBlogPost(Document):
|
||||
meta = {'abstract': True}
|
||||
authors = ListField(ReferenceField(self.Person,
|
||||
reverse_delete_rule=PULL))
|
||||
|
||||
class BlogPost(AbstractBlogPost):
|
||||
content = StringField()
|
||||
|
||||
BlogPost.drop_collection()
|
||||
self.Person.drop_collection()
|
||||
|
||||
me = self.Person(name='Test User')
|
||||
me.save()
|
||||
|
||||
someoneelse = self.Person(name='Some-one Else')
|
||||
someoneelse.save()
|
||||
|
||||
post = BlogPost(content='Watching TV', authors=[me, someoneelse])
|
||||
post.save()
|
||||
|
||||
another = BlogPost(content='Chilling Out', authors=[someoneelse])
|
||||
another.save()
|
||||
|
||||
someoneelse.delete()
|
||||
post.reload()
|
||||
another.reload()
|
||||
|
||||
self.assertEqual(post.authors, [me])
|
||||
self.assertEqual(another.authors, [])
|
||||
|
||||
def test_delete_with_limits(self):
|
||||
|
||||
class Log(Document):
|
||||
@@ -1492,6 +1607,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
"""Ensure that atomic updates work properly.
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
name = StringField()
|
||||
title = StringField()
|
||||
hits = IntField()
|
||||
tags = ListField(StringField())
|
||||
@@ -2783,25 +2899,23 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertTrue('dilma' in new.content)
|
||||
self.assertTrue('planejamento' in new.title)
|
||||
|
||||
query = News.objects.search_text(
|
||||
"candidata", include_text_scores=True)
|
||||
|
||||
self.assertTrue(query._include_text_scores)
|
||||
query = News.objects.search_text("candidata")
|
||||
self.assertEqual(query._search_text, "candidata")
|
||||
new = query.first()
|
||||
|
||||
self.assertTrue(isinstance(new.text_score, float))
|
||||
self.assertTrue(isinstance(new.get_text_score(), float))
|
||||
|
||||
# count
|
||||
query = News.objects.search_text('brasil').order_by('$text_score')
|
||||
self.assertTrue(query._include_text_scores)
|
||||
self.assertEqual(query._search_text, "brasil")
|
||||
|
||||
self.assertEqual(query.count(), 3)
|
||||
self.assertEqual(query._query, {'$text': {'$search': 'brasil'}})
|
||||
cursor_args = query._cursor_args
|
||||
self.assertEqual(
|
||||
cursor_args['fields'], {'text_score': {'$meta': 'textScore'}})
|
||||
cursor_args['fields'], {'_text_score': {'$meta': 'textScore'}})
|
||||
|
||||
text_scores = [i.text_score for i in query]
|
||||
text_scores = [i.get_text_score() for i in query]
|
||||
self.assertEqual(len(text_scores), 3)
|
||||
|
||||
self.assertTrue(text_scores[0] > text_scores[1])
|
||||
@@ -2811,7 +2925,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
# get item
|
||||
item = News.objects.search_text(
|
||||
'brasil').order_by('$text_score').first()
|
||||
self.assertEqual(item.text_score, max_text_score)
|
||||
self.assertEqual(item.get_text_score(), max_text_score)
|
||||
|
||||
@skip_older_mongodb
|
||||
def test_distinct_handles_references_to_alias(self):
|
||||
@@ -2878,13 +2992,55 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(authors, [mark_twain, john_tolkien])
|
||||
|
||||
def test_distinct_ListField_EmbeddedDocumentField_EmbeddedDocumentField(self):
|
||||
class Continent(EmbeddedDocument):
|
||||
continent_name = StringField()
|
||||
|
||||
class Country(EmbeddedDocument):
|
||||
country_name = StringField()
|
||||
continent = EmbeddedDocumentField(Continent)
|
||||
|
||||
class Author(EmbeddedDocument):
|
||||
name = StringField()
|
||||
country = EmbeddedDocumentField(Country)
|
||||
|
||||
class Book(Document):
|
||||
title = StringField()
|
||||
authors = ListField(EmbeddedDocumentField(Author))
|
||||
|
||||
Book.drop_collection()
|
||||
|
||||
europe = Continent(continent_name='europe')
|
||||
asia = Continent(continent_name='asia')
|
||||
|
||||
scotland = Country(country_name="Scotland", continent=europe)
|
||||
tibet = Country(country_name="Tibet", continent=asia)
|
||||
|
||||
mark_twain = Author(name="Mark Twain", country=scotland)
|
||||
john_tolkien = Author(name="John Ronald Reuel Tolkien", country=tibet)
|
||||
|
||||
book = Book(title="Tom Sawyer", authors=[mark_twain]).save()
|
||||
book = Book(
|
||||
title="The Lord of the Rings", authors=[john_tolkien]).save()
|
||||
book = Book(
|
||||
title="The Stories", authors=[mark_twain, john_tolkien]).save()
|
||||
country_list = Book.objects.distinct("authors.country")
|
||||
|
||||
self.assertEqual(country_list, [scotland, tibet])
|
||||
|
||||
continent_list = Book.objects.distinct("authors.country.continent")
|
||||
|
||||
self.assertEqual(continent_list, [europe, asia])
|
||||
|
||||
def test_distinct_ListField_ReferenceField(self):
|
||||
class Foo(Document):
|
||||
bar_lst = ListField(ReferenceField('Bar'))
|
||||
|
||||
class Bar(Document):
|
||||
text = StringField()
|
||||
|
||||
class Foo(Document):
|
||||
bar = ReferenceField('Bar')
|
||||
bar_lst = ListField(ReferenceField('Bar'))
|
||||
|
||||
Bar.drop_collection()
|
||||
Foo.drop_collection()
|
||||
|
||||
@@ -3244,7 +3400,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
for i in xrange(10):
|
||||
Post(title="Post %s" % i).save()
|
||||
|
||||
self.assertEqual(5, Post.objects.limit(5).skip(5).count())
|
||||
self.assertEqual(5, Post.objects.limit(5).skip(5).count(with_limit_and_skip=True))
|
||||
|
||||
self.assertEqual(
|
||||
10, Post.objects.limit(5).skip(5).count(with_limit_and_skip=False))
|
||||
@@ -4013,7 +4169,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertEqual(100, people._len) # Caused by list calling len
|
||||
self.assertEqual(q, 1)
|
||||
|
||||
people.count() # count is cached
|
||||
people.count(with_limit_and_skip=True) # count is cached
|
||||
self.assertEqual(q, 1)
|
||||
|
||||
def test_no_cached_queryset(self):
|
||||
@@ -4109,7 +4265,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
with query_counter() as q:
|
||||
self.assertEqual(q, 0)
|
||||
|
||||
self.assertEqual(users.count(), 7)
|
||||
self.assertEqual(users.count(with_limit_and_skip=True), 7)
|
||||
|
||||
for i, outer_user in enumerate(users):
|
||||
self.assertEqual(outer_user.name, names[i])
|
||||
@@ -4117,7 +4273,7 @@ class QuerySetTest(unittest.TestCase):
|
||||
inner_count = 0
|
||||
|
||||
# Calling len might disrupt the inner loop if there are bugs
|
||||
self.assertEqual(users.count(), 7)
|
||||
self.assertEqual(users.count(with_limit_and_skip=True), 7)
|
||||
|
||||
for j, inner_user in enumerate(users):
|
||||
self.assertEqual(inner_user.name, names[j])
|
||||
@@ -4401,6 +4557,27 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.Person.objects().delete()
|
||||
self.assertEqual(self.Person.objects().skip(1).delete(), 0) # test Document delete without existing documents
|
||||
|
||||
def test_max_time_ms(self):
|
||||
# 778: max_time_ms can get only int or None as input
|
||||
self.assertRaises(TypeError, self.Person.objects(name="name").max_time_ms, "not a number")
|
||||
|
||||
def test_subclass_field_query(self):
|
||||
class Animal(Document):
|
||||
is_mamal = BooleanField()
|
||||
meta = dict(allow_inheritance=True)
|
||||
|
||||
class Cat(Animal):
|
||||
whiskers_length = FloatField()
|
||||
|
||||
class ScottishCat(Cat):
|
||||
folded_ears = BooleanField()
|
||||
|
||||
Animal(is_mamal=False).save()
|
||||
Cat(is_mamal=True, whiskers_length=5.1).save()
|
||||
ScottishCat(is_mamal=True, folded_ears=True).save()
|
||||
self.assertEquals(Animal.objects(folded_ears=True).count(), 1)
|
||||
self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -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]]]})
|
||||
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__':
|
||||
unittest.main()
|
||||
|
@@ -147,6 +147,18 @@ class ConnectionTest(unittest.TestCase):
|
||||
date_doc = DateDoc.objects.first()
|
||||
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__':
|
||||
unittest.main()
|
||||
|
@@ -318,6 +318,10 @@ class FieldTest(unittest.TestCase):
|
||||
def test_circular_reference(self):
|
||||
"""Ensure you can handle circular references
|
||||
"""
|
||||
class Relation(EmbeddedDocument):
|
||||
name = StringField()
|
||||
person = ReferenceField('Person')
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
relations = ListField(EmbeddedDocumentField('Relation'))
|
||||
@@ -325,10 +329,6 @@ class FieldTest(unittest.TestCase):
|
||||
def __repr__(self):
|
||||
return "<Person: %s>" % self.name
|
||||
|
||||
class Relation(EmbeddedDocument):
|
||||
name = StringField()
|
||||
person = ReferenceField('Person')
|
||||
|
||||
Person.drop_collection()
|
||||
mother = Person(name="Mother")
|
||||
daughter = Person(name="Daughter")
|
||||
@@ -947,6 +947,8 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
class Asset(Document):
|
||||
name = StringField(max_length=250, required=True)
|
||||
path = StringField()
|
||||
title = StringField()
|
||||
parent = GenericReferenceField(default=None)
|
||||
parents = ListField(GenericReferenceField())
|
||||
children = ListField(GenericReferenceField())
|
||||
@@ -1220,14 +1222,15 @@ class FieldTest(unittest.TestCase):
|
||||
self.assertEqual(page.tags[0], page.posts[0].tags[0])
|
||||
|
||||
def test_select_related_follows_embedded_referencefields(self):
|
||||
class Playlist(Document):
|
||||
items = ListField(EmbeddedDocumentField("PlaylistItem"))
|
||||
|
||||
class Song(Document):
|
||||
title = StringField()
|
||||
|
||||
class PlaylistItem(EmbeddedDocument):
|
||||
song = ReferenceField("Song")
|
||||
|
||||
class Song(Document):
|
||||
title = StringField()
|
||||
class Playlist(Document):
|
||||
items = ListField(EmbeddedDocumentField("PlaylistItem"))
|
||||
|
||||
Playlist.drop_collection()
|
||||
Song.drop_collection()
|
||||
|
@@ -19,9 +19,12 @@ settings.configure(
|
||||
AUTHENTICATION_BACKENDS = ('mongoengine.django.auth.MongoEngineBackend',)
|
||||
)
|
||||
|
||||
# For Django >= 1.7
|
||||
if hasattr(django, 'setup'):
|
||||
django.setup()
|
||||
try:
|
||||
# For Django >= 1.7
|
||||
if hasattr(django, 'setup'):
|
||||
django.setup()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from django.contrib.auth import authenticate, get_user_model
|
||||
@@ -34,7 +37,6 @@ try:
|
||||
DJ15 = True
|
||||
except Exception:
|
||||
DJ15 = False
|
||||
from django.contrib.sessions.tests import SessionTestsMixin
|
||||
from mongoengine.django.sessions import SessionStore, MongoSession
|
||||
from mongoengine.django.tests import MongoTestCase
|
||||
from datetime import tzinfo, timedelta
|
||||
@@ -170,8 +172,13 @@ class QuerySetTest(unittest.TestCase):
|
||||
"""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):
|
||||
text = StringField()
|
||||
meta = dict(queryset_class=LimitCountQuerySet)
|
||||
name = StringField()
|
||||
|
||||
Note.drop_collection()
|
||||
|
||||
@@ -221,13 +228,13 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertEqual(t.render(c), "10")
|
||||
|
||||
|
||||
class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
|
||||
class _BaseMongoDBSessionTest(unittest.TestCase):
|
||||
backend = SessionStore
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
MongoSession.drop_collection()
|
||||
super(MongoDBSessionTest, self).setUp()
|
||||
super(_BaseMongoDBSessionTest, self).setUp()
|
||||
|
||||
def assertIn(self, first, second, msg=None):
|
||||
self.assertTrue(first in second, msg)
|
||||
@@ -254,6 +261,21 @@ class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
|
||||
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):
|
||||
user_data = {
|
||||
'username': 'user',
|
||||
|
Reference in New Issue
Block a user