Compare commits
58 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6a02ac7e80 | ||
|
d1b86fdef5 | ||
|
57ac38ddca | ||
|
7a73a92074 | ||
|
d1b30f4792 | ||
|
16dcf78cab | ||
|
d868cfdeb0 | ||
|
c074f4d925 | ||
|
453024c58d | ||
|
fe8340617a | ||
|
b024dd913d | ||
|
a2a698ab0e | ||
|
bb56f92213 | ||
|
8dcd998945 | ||
|
bcbbbe4046 | ||
|
7200a8cb84 | ||
|
6925344807 | ||
|
60ceeb0ddd | ||
|
06caabf333 | ||
|
954131bd51 | ||
|
855efe7fe8 | ||
|
d902a74ab0 | ||
|
499e11f730 | ||
|
6db59a9c31 | ||
|
6465726008 | ||
|
3a3b96e0be | ||
|
992c91dc0c | ||
|
809473c15c | ||
|
d79a5ec3d6 | ||
|
237469ceaf | ||
|
c28d9135d9 | ||
|
48a5679087 | ||
|
7c938712f2 | ||
|
4df12bebc2 | ||
|
dfe8987aaa | ||
|
02dbe401d8 | ||
|
c18f8c92e7 | ||
|
11d4f6499a | ||
|
f2c25b4744 | ||
|
27b846717f | ||
|
9ed138f896 | ||
|
1978dc80eb | ||
|
fc4b247f4f | ||
|
ebf7056f4a | ||
|
eb975d7e13 | ||
|
a2dd8cb6b9 | ||
|
7c254c6136 | ||
|
c8a33b83f1 | ||
|
1145c72b01 | ||
|
7fc45fb711 | ||
|
e146262c38 | ||
|
6f808bd06e | ||
|
0b6ab49325 | ||
|
66d9182e50 | ||
|
654cca82a9 | ||
|
89785da1c5 | ||
|
2f9964e46e | ||
|
168ecd67b0 |
16
.travis.yml
16
.travis.yml
@@ -7,12 +7,16 @@ python:
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
env:
|
||||
- PYMONGO=dev DJANGO=1.5.1
|
||||
- PYMONGO=dev DJANGO=1.4.2
|
||||
- PYMONGO=2.5 DJANGO=1.5.1
|
||||
- PYMONGO=2.5 DJANGO=1.4.2
|
||||
- PYMONGO=3.2 DJANGO=1.5.1
|
||||
- PYMONGO=3.3 DJANGO=1.5.1
|
||||
- PYMONGO=dev DJANGO=1.6
|
||||
- PYMONGO=dev DJANGO=1.5.5
|
||||
- PYMONGO=dev DJANGO=1.4.10
|
||||
- PYMONGO=2.5 DJANGO=1.6
|
||||
- PYMONGO=2.5 DJANGO=1.5.5
|
||||
- PYMONGO=2.5 DJANGO=1.4.10
|
||||
- PYMONGO=3.2 DJANGO=1.6
|
||||
- PYMONGO=3.2 DJANGO=1.5.5
|
||||
- PYMONGO=3.3 DJANGO=1.6
|
||||
- PYMONGO=3.3 DJANGO=1.5.5
|
||||
install:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi
|
||||
|
12
AUTHORS
12
AUTHORS
@@ -134,7 +134,7 @@ that much better:
|
||||
* Paul Swartz
|
||||
* Sundar Raman
|
||||
* Benoit Louy
|
||||
* lraucy
|
||||
* Loic Raucy (https://github.com/lraucy)
|
||||
* hellysmile
|
||||
* Jaepil Jeong
|
||||
* Daniil Sharou
|
||||
@@ -179,3 +179,13 @@ that much better:
|
||||
* crazyzubr (https://github.com/crazyzubr)
|
||||
* FrankSomething (https://github.com/FrankSomething)
|
||||
* Alexandr Morozov (https://github.com/LK4D4)
|
||||
* mishudark (https://github.com/mishudark)
|
||||
* Joe Friedl (https://github.com/grampajoe)
|
||||
* Daniel Ward (https://github.com/danielward)
|
||||
* Aniket Deshpande (https://github.com/anicake)
|
||||
* rfkrocktk (https://github.com/rfkrocktk)
|
||||
* Gustavo Andrés Angulo (https://github.com/woakas)
|
||||
* Dmytro Popovych (https://github.com/drudim)
|
||||
* Tom (https://github.com/tomprimozic)
|
||||
* j0hnsmith (https://github.com/j0hnsmith)
|
||||
* Damien Churchill (https://github.com/damoxc)
|
||||
|
@@ -2,6 +2,28 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Changes in 0.8.6
|
||||
================
|
||||
- Fix django auth import (#531)
|
||||
|
||||
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)
|
||||
- Fixed EmbeddedDocument with ReferenceField equality issue (#502)
|
||||
- Fixed GenericReferenceField serialization order (#499)
|
||||
- Fixed count and none bug (#498)
|
||||
- Fixed bug with .only() and DictField with digit keys (#496)
|
||||
- Added user_permissions to Django User object (#491, #492)
|
||||
- Fix updating Geo Location fields (#488)
|
||||
- Fix handling invalid dict field value (#485)
|
||||
- Added app_label to MongoUser (#484)
|
||||
- Use defaults when host and port are passed as None (#483)
|
||||
- Fixed distinct casting issue with ListField of EmbeddedDocuments (#470)
|
||||
- Fixed Django 1.6 sessions (#454, #480)
|
||||
|
||||
Changes in 0.8.4
|
||||
================
|
||||
- Remove database name necessity in uri connection schema (#452)
|
||||
|
@@ -90,10 +90,15 @@ session backend, ensure that your settings module has
|
||||
into your settings module::
|
||||
|
||||
SESSION_ENGINE = 'mongoengine.django.sessions'
|
||||
SESSION_SERIALIZER = 'mongoengine.django.sessions.BSONSerializer'
|
||||
|
||||
Django provides session cookie, which expires after ```SESSION_COOKIE_AGE``` seconds, but doesn't delete cookie at sessions backend, so ``'mongoengine.django.sessions'`` supports `mongodb TTL
|
||||
<http://docs.mongodb.org/manual/tutorial/expire-data/>`_.
|
||||
|
||||
.. note:: ``SESSION_SERIALIZER`` is only necessary in Django 1.6 as the default
|
||||
serializer is based around JSON and doesn't know how to convert
|
||||
``bson.objectid.ObjectId`` instances to strings.
|
||||
|
||||
.. versionadded:: 0.2.1
|
||||
|
||||
Storage
|
||||
|
@@ -290,6 +290,12 @@ instance of the object to the query::
|
||||
# Find all pages that both Bob and John have authored
|
||||
Page.objects(authors__all=[bob, john])
|
||||
|
||||
# Remove Bob from the authors for a page.
|
||||
Page.objects(id='...').update_one(pull__authors=bob)
|
||||
|
||||
# Add John to the authors for a page.
|
||||
Page.objects(id='...').update_one(push__authors=john)
|
||||
|
||||
|
||||
Dealing with deletion of referred documents
|
||||
'''''''''''''''''''''''''''''''''''''''''''
|
||||
|
@@ -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, 4)
|
||||
VERSION = (0, 8, 6)
|
||||
|
||||
|
||||
def get_version():
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import copy
|
||||
import operator
|
||||
import numbers
|
||||
from collections import Hashable
|
||||
from functools import partial
|
||||
|
||||
import pymongo
|
||||
@@ -375,20 +376,41 @@ class BaseDocument(object):
|
||||
self._changed_fields.append(key)
|
||||
|
||||
def _clear_changed_fields(self):
|
||||
"""Using get_changed_fields iterate and remove any fields that are
|
||||
marked as changed"""
|
||||
for changed in self._get_changed_fields():
|
||||
parts = changed.split(".")
|
||||
data = self
|
||||
for part in parts:
|
||||
if isinstance(data, list):
|
||||
try:
|
||||
data = data[int(part)]
|
||||
except IndexError:
|
||||
data = None
|
||||
elif isinstance(data, dict):
|
||||
data = data.get(part, None)
|
||||
else:
|
||||
data = getattr(data, part, None)
|
||||
if hasattr(data, "_changed_fields"):
|
||||
data._changed_fields = []
|
||||
self._changed_fields = []
|
||||
EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
|
||||
for field_name, field in self._fields.iteritems():
|
||||
if (isinstance(field, ComplexBaseField) and
|
||||
isinstance(field.field, EmbeddedDocumentField)):
|
||||
field_value = getattr(self, field_name, None)
|
||||
if field_value:
|
||||
for idx in (field_value if isinstance(field_value, dict)
|
||||
else xrange(len(field_value))):
|
||||
field_value[idx]._clear_changed_fields()
|
||||
elif isinstance(field, EmbeddedDocumentField):
|
||||
field_value = getattr(self, field_name, None)
|
||||
if field_value:
|
||||
field_value._clear_changed_fields()
|
||||
|
||||
def _nestable_types_changed_fields(self, changed_fields, key, data, inspected):
|
||||
# Loop list / dict fields as they contain documents
|
||||
# Determine the iterator to use
|
||||
if not hasattr(data, 'items'):
|
||||
iterator = enumerate(data)
|
||||
else:
|
||||
iterator = data.iteritems()
|
||||
|
||||
for index, value in iterator:
|
||||
list_key = "%s%s." % (key, index)
|
||||
if hasattr(value, '_get_changed_fields'):
|
||||
changed = value._get_changed_fields(inspected)
|
||||
changed_fields += ["%s%s" % (list_key, k)
|
||||
for k in changed if k]
|
||||
elif isinstance(value, (list, tuple, dict)):
|
||||
self._nestable_types_changed_fields(changed_fields, list_key, value, inspected)
|
||||
|
||||
def _get_changed_fields(self, inspected=None):
|
||||
"""Returns a list of all fields that have explicitly been changed.
|
||||
@@ -396,13 +418,12 @@ class BaseDocument(object):
|
||||
EmbeddedDocument = _import_class("EmbeddedDocument")
|
||||
DynamicEmbeddedDocument = _import_class("DynamicEmbeddedDocument")
|
||||
ReferenceField = _import_class("ReferenceField")
|
||||
_changed_fields = []
|
||||
_changed_fields += getattr(self, '_changed_fields', [])
|
||||
|
||||
changed_fields = []
|
||||
changed_fields += getattr(self, '_changed_fields', [])
|
||||
inspected = inspected or set()
|
||||
if hasattr(self, 'id'):
|
||||
if hasattr(self, 'id') and isinstance(self.id, Hashable):
|
||||
if self.id in inspected:
|
||||
return _changed_fields
|
||||
return changed_fields
|
||||
inspected.add(self.id)
|
||||
|
||||
for field_name in self._fields_ordered:
|
||||
@@ -418,29 +439,17 @@ class BaseDocument(object):
|
||||
if isinstance(field, ReferenceField):
|
||||
continue
|
||||
elif (isinstance(data, (EmbeddedDocument, DynamicEmbeddedDocument))
|
||||
and db_field_name not in _changed_fields):
|
||||
and db_field_name not in changed_fields):
|
||||
# Find all embedded fields that have been changed
|
||||
changed = data._get_changed_fields(inspected)
|
||||
_changed_fields += ["%s%s" % (key, k) for k in changed if k]
|
||||
changed_fields += ["%s%s" % (key, k) for k in changed if k]
|
||||
elif (isinstance(data, (list, tuple, dict)) and
|
||||
db_field_name not in _changed_fields):
|
||||
# Loop list / dict fields as they contain documents
|
||||
# Determine the iterator to use
|
||||
if not hasattr(data, 'items'):
|
||||
iterator = enumerate(data)
|
||||
else:
|
||||
iterator = data.iteritems()
|
||||
for index, value in iterator:
|
||||
if not hasattr(value, '_get_changed_fields'):
|
||||
continue
|
||||
if (hasattr(field, 'field') and
|
||||
isinstance(field.field, ReferenceField)):
|
||||
continue
|
||||
list_key = "%s%s." % (key, index)
|
||||
changed = value._get_changed_fields(inspected)
|
||||
_changed_fields += ["%s%s" % (list_key, k)
|
||||
for k in changed if k]
|
||||
return _changed_fields
|
||||
db_field_name not in changed_fields):
|
||||
if (hasattr(field, 'field') and
|
||||
isinstance(field.field, ReferenceField)):
|
||||
continue
|
||||
self._nestable_types_changed_fields(changed_fields, key, data, inspected)
|
||||
return changed_fields
|
||||
|
||||
def _delta(self):
|
||||
"""Returns the delta (set, unset) of the changes for a document.
|
||||
@@ -755,6 +764,9 @@ class BaseDocument(object):
|
||||
"""Lookup a field based on its attribute and return a list containing
|
||||
the field's parents and the field.
|
||||
"""
|
||||
|
||||
ListField = _import_class("ListField")
|
||||
|
||||
if not isinstance(parts, (list, tuple)):
|
||||
parts = [parts]
|
||||
fields = []
|
||||
@@ -762,7 +774,7 @@ class BaseDocument(object):
|
||||
|
||||
for field_name in parts:
|
||||
# Handle ListField indexing:
|
||||
if field_name.isdigit() and hasattr(field, 'field'):
|
||||
if field_name.isdigit() and isinstance(field, ListField):
|
||||
new_field = field.field
|
||||
fields.append(field_name)
|
||||
continue
|
||||
|
@@ -89,12 +89,7 @@ class BaseField(object):
|
||||
return self
|
||||
|
||||
# Get value from document instance if available
|
||||
value = instance._data.get(self.name)
|
||||
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
if isinstance(value, EmbeddedDocument) and value._instance is None:
|
||||
value._instance = weakref.proxy(instance)
|
||||
return value
|
||||
return instance._data.get(self.name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""Descriptor for assigning a value to a field in a document.
|
||||
@@ -116,6 +111,10 @@ class BaseField(object):
|
||||
# Values cant be compared eg: naive and tz datetimes
|
||||
# So mark it as changed
|
||||
instance._mark_as_changed(self.name)
|
||||
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
if isinstance(value, EmbeddedDocument) and value._instance is None:
|
||||
value._instance = weakref.proxy(instance)
|
||||
instance._data[self.name] = value
|
||||
|
||||
def error(self, message="", errors=None, field_name=None):
|
||||
@@ -203,7 +202,7 @@ class ComplexBaseField(BaseField):
|
||||
_dereference = _import_class("DeReference")()
|
||||
|
||||
self._auto_dereference = instance._fields[self.name]._auto_dereference
|
||||
if instance._initialised and dereference:
|
||||
if instance._initialised and dereference and instance._data.get(self.name):
|
||||
instance._data[self.name] = _dereference(
|
||||
instance._data.get(self.name), max_depth=1, instance=instance,
|
||||
name=self.name
|
||||
|
@@ -25,7 +25,7 @@ def _import_class(cls_name):
|
||||
'GenericEmbeddedDocumentField', 'GeoPointField',
|
||||
'PointField', 'LineStringField', 'ListField',
|
||||
'PolygonField', 'ReferenceField', 'StringField',
|
||||
'ComplexBaseField')
|
||||
'ComplexBaseField', 'GeoJsonBaseField')
|
||||
queryset_classes = ('OperationError',)
|
||||
deref_classes = ('DeReference',)
|
||||
|
||||
|
@@ -18,7 +18,7 @@ _connections = {}
|
||||
_dbs = {}
|
||||
|
||||
|
||||
def register_connection(alias, name, host='localhost', port=27017,
|
||||
def register_connection(alias, name, host=None, port=None,
|
||||
is_slave=False, read_preference=False, slaves=None,
|
||||
username=None, password=None, **kwargs):
|
||||
"""Add a connection.
|
||||
@@ -43,8 +43,8 @@ def register_connection(alias, name, host='localhost', port=27017,
|
||||
|
||||
conn_settings = {
|
||||
'name': name,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'host': host or 'localhost',
|
||||
'port': port or 27017,
|
||||
'is_slave': is_slave,
|
||||
'slaves': slaves or [],
|
||||
'username': username,
|
||||
@@ -53,16 +53,15 @@ def register_connection(alias, name, host='localhost', port=27017,
|
||||
}
|
||||
|
||||
# Handle uri style connections
|
||||
if "://" in host:
|
||||
uri_dict = uri_parser.parse_uri(host)
|
||||
if "://" in conn_settings['host']:
|
||||
uri_dict = uri_parser.parse_uri(conn_settings['host'])
|
||||
conn_settings.update({
|
||||
'host': host,
|
||||
'name': uri_dict.get('database') or name,
|
||||
'username': uri_dict.get('username'),
|
||||
'password': uri_dict.get('password'),
|
||||
'read_preference': read_preference,
|
||||
})
|
||||
if "replicaSet" in host:
|
||||
if "replicaSet" in conn_settings['host']:
|
||||
conn_settings['replicaSet'] = True
|
||||
|
||||
conn_settings.update(kwargs)
|
||||
|
@@ -8,6 +8,11 @@ from django.contrib import auth
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .utils import datetime_now
|
||||
from .mongo_auth.models import get_user_document
|
||||
|
||||
REDIRECT_FIELD_NAME = 'next'
|
||||
|
||||
try:
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
except ImportError:
|
||||
@@ -33,10 +38,6 @@ except ImportError:
|
||||
hash = get_hexdigest(algo, salt, raw_password)
|
||||
return '%s$%s$%s' % (algo, salt, hash)
|
||||
|
||||
from .utils import datetime_now
|
||||
|
||||
REDIRECT_FIELD_NAME = 'next'
|
||||
|
||||
|
||||
class ContentType(Document):
|
||||
name = StringField(max_length=100)
|
||||
@@ -230,6 +231,9 @@ class User(Document):
|
||||
date_joined = DateTimeField(default=datetime_now,
|
||||
verbose_name=_('date joined'))
|
||||
|
||||
user_permissions = ListField(ReferenceField(Permission), verbose_name=_('user permissions'),
|
||||
help_text=_('Permissions for the user.'))
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
REQUIRED_FIELDS = ['email']
|
||||
|
||||
@@ -380,7 +384,7 @@ class MongoEngineBackend(object):
|
||||
supports_inactive_user = False
|
||||
|
||||
def authenticate(self, username=None, password=None):
|
||||
user = User.objects(username=username).first()
|
||||
user = get_user_document().objects(username=username).first()
|
||||
if user:
|
||||
if password and user.check_password(password):
|
||||
backend = auth.get_backends()[0]
|
||||
@@ -389,7 +393,7 @@ class MongoEngineBackend(object):
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
return User.objects.with_id(user_id)
|
||||
return get_user_document().objects.with_id(user_id)
|
||||
|
||||
|
||||
def get_user(userid):
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from django.conf import settings
|
||||
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
|
||||
@@ -105,3 +106,10 @@ class MongoUser(models.Model):
|
||||
"""
|
||||
|
||||
objects = MongoUserManager()
|
||||
|
||||
class Meta:
|
||||
app_label = 'mongo_auth'
|
||||
|
||||
def set_password(self, password):
|
||||
"""Doesn't do anything, but works around the issue with Django 1.6."""
|
||||
make_password(password)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
from bson import json_util
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
@@ -55,6 +56,12 @@ class SessionStore(SessionBase):
|
||||
"""A MongoEngine-based session store for Django.
|
||||
"""
|
||||
|
||||
def _get_session(self, *args, **kwargs):
|
||||
sess = super(SessionStore, self)._get_session(*args, **kwargs)
|
||||
if sess.get('_auth_user_id', None):
|
||||
sess['_auth_user_id'] = str(sess.get('_auth_user_id'))
|
||||
return sess
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
s = MongoSession.objects(session_key=self.session_key,
|
||||
@@ -103,3 +110,15 @@ class SessionStore(SessionBase):
|
||||
return
|
||||
session_key = self.session_key
|
||||
MongoSession.objects(session_key=session_key).delete()
|
||||
|
||||
|
||||
class BSONSerializer(object):
|
||||
"""
|
||||
Serializer that can handle BSON types (eg ObjectId).
|
||||
"""
|
||||
def dumps(self, obj):
|
||||
return json_util.dumps(obj, separators=(',', ':')).encode('ascii')
|
||||
|
||||
def loads(self, data):
|
||||
return json_util.loads(data.decode('ascii'))
|
||||
|
||||
|
@@ -76,7 +76,7 @@ class GridFSStorage(Storage):
|
||||
"""Find the documents in the store with the given name
|
||||
"""
|
||||
docs = self.document.objects
|
||||
doc = [d for d in docs if getattr(d, self.field).name == name]
|
||||
doc = [d for d in docs if hasattr(getattr(d, self.field), 'name') and getattr(d, self.field).name == name]
|
||||
if doc:
|
||||
return doc[0]
|
||||
else:
|
||||
|
@@ -66,7 +66,7 @@ class EmbeddedDocument(BaseDocument):
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self._data == other._data
|
||||
return self.to_mongo() == other.to_mongo()
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
|
@@ -42,7 +42,8 @@ __all__ = ['StringField', 'URLField', 'EmailField', 'IntField', 'LongField',
|
||||
'GenericReferenceField', 'BinaryField', 'GridFSError',
|
||||
'GridFSProxy', 'FileField', 'ImageGridFsProxy',
|
||||
'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField',
|
||||
'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField']
|
||||
'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField',
|
||||
'GeoJsonBaseField']
|
||||
|
||||
|
||||
RECURSIVE_REFERENCE_CONSTANT = 'self'
|
||||
@@ -304,7 +305,10 @@ class DecimalField(BaseField):
|
||||
return value
|
||||
|
||||
# Convert to string for python 2.6 before casting to Decimal
|
||||
value = decimal.Decimal("%s" % value)
|
||||
try:
|
||||
value = decimal.Decimal("%s" % value)
|
||||
except decimal.InvalidOperation:
|
||||
return value
|
||||
return value.quantize(self.precision, rounding=self.rounding)
|
||||
|
||||
def to_mongo(self, value):
|
||||
@@ -735,6 +739,21 @@ class SortedListField(ListField):
|
||||
reverse=self._order_reverse)
|
||||
return sorted(value, reverse=self._order_reverse)
|
||||
|
||||
def key_not_string(d):
|
||||
""" Helper function to recursively determine if any key in a dictionary is
|
||||
not a string.
|
||||
"""
|
||||
for k, v in d.items():
|
||||
if not isinstance(k, basestring) or (isinstance(v, dict) and key_not_string(v)):
|
||||
return True
|
||||
|
||||
def key_has_dot_or_dollar(d):
|
||||
""" Helper function to recursively determine if any key in a dictionary
|
||||
contains a dot or a dollar sign.
|
||||
"""
|
||||
for k, v in d.items():
|
||||
if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)):
|
||||
return True
|
||||
|
||||
class DictField(ComplexBaseField):
|
||||
"""A dictionary field that wraps a standard Python dictionary. This is
|
||||
@@ -761,11 +780,11 @@ class DictField(ComplexBaseField):
|
||||
if not isinstance(value, dict):
|
||||
self.error('Only dictionaries may be used in a DictField')
|
||||
|
||||
if any(k for k in value.keys() if not isinstance(k, basestring)):
|
||||
if key_not_string(value):
|
||||
msg = ("Invalid dictionary key - documents must "
|
||||
"have only string keys")
|
||||
self.error(msg)
|
||||
if any(('.' in k or '$' in k) for k in value.keys()):
|
||||
if key_has_dot_or_dollar(value):
|
||||
self.error('Invalid dictionary key name - keys may not contain "."'
|
||||
' or "$" characters')
|
||||
super(DictField, self).validate(value)
|
||||
@@ -1004,7 +1023,10 @@ class GenericReferenceField(BaseField):
|
||||
id_ = id_field.to_mongo(id_)
|
||||
collection = document._get_collection_name()
|
||||
ref = DBRef(collection, id_)
|
||||
return {'_cls': document._class_name, '_ref': ref}
|
||||
return SON((
|
||||
('_cls', document._class_name),
|
||||
('_ref', ref)
|
||||
))
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
if value is None:
|
||||
|
@@ -331,7 +331,7 @@ class BaseQuerySet(object):
|
||||
:meth:`skip` that has been applied to this cursor into account when
|
||||
getting the count
|
||||
"""
|
||||
if self._limit == 0 and with_limit_and_skip:
|
||||
if self._limit == 0 and with_limit_and_skip or self._none:
|
||||
return 0
|
||||
return self._cursor.count(with_limit_and_skip=with_limit_and_skip)
|
||||
|
||||
@@ -621,8 +621,15 @@ class BaseQuerySet(object):
|
||||
try:
|
||||
field = self._fields_to_dbfields([field]).pop()
|
||||
finally:
|
||||
return self._dereference(queryset._cursor.distinct(field), 1,
|
||||
name=field, instance=self._document)
|
||||
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)
|
||||
if instance:
|
||||
distinct = [instance(**doc) for doc in distinct]
|
||||
return distinct
|
||||
|
||||
def only(self, *fields):
|
||||
"""Load only a subset of this document's fields. ::
|
||||
@@ -850,7 +857,7 @@ class BaseQuerySet(object):
|
||||
:param output: output collection name, if set to 'inline' will try to
|
||||
use :class:`~pymongo.collection.Collection.inline_map_reduce`
|
||||
This can also be a dictionary containing output options
|
||||
see: http://docs.mongodb.org/manual/reference/commands/#mapReduce
|
||||
see: http://docs.mongodb.org/manual/reference/command/mapReduce/#dbcmd.mapReduce
|
||||
:param finalize_f: finalize function, an optional function that
|
||||
performs any post-reduction processing.
|
||||
:param scope: values to insert into map/reduce global scope. Optional.
|
||||
@@ -1491,4 +1498,4 @@ class BaseQuerySet(object):
|
||||
msg = ("Doc.objects()._ensure_indexes() is deprecated. "
|
||||
"Use Doc.ensure_indexes() instead.")
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
self._document.__class__.ensure_indexes()
|
||||
self._document.__class__.ensure_indexes()
|
||||
|
@@ -206,6 +206,10 @@ def update(_doc_cls=None, **update):
|
||||
else:
|
||||
field = cleaned_fields[-1]
|
||||
|
||||
GeoJsonBaseField = _import_class("GeoJsonBaseField")
|
||||
if isinstance(field, GeoJsonBaseField):
|
||||
value = field.to_mongo(value)
|
||||
|
||||
if op in (None, 'set', 'push', 'pull'):
|
||||
if field.required or value is not None:
|
||||
value = field.prepare_query_value(op, value)
|
||||
|
@@ -5,7 +5,7 @@
|
||||
%define srcname mongoengine
|
||||
|
||||
Name: python-%{srcname}
|
||||
Version: 0.8.4
|
||||
Version: 0.8.6
|
||||
Release: 1%{?dist}
|
||||
Summary: A Python Document-Object Mapper for working with MongoDB
|
||||
|
||||
|
@@ -713,6 +713,27 @@ class DeltaTest(unittest.TestCase):
|
||||
self.assertEqual({}, removals)
|
||||
self.assertTrue('employees' in updates)
|
||||
|
||||
def test_nested_nested_fields_mark_as_changed(self):
|
||||
class EmbeddedDoc(EmbeddedDocument):
|
||||
name = StringField()
|
||||
|
||||
class MyDoc(Document):
|
||||
subs = MapField(MapField(EmbeddedDocumentField(EmbeddedDoc)))
|
||||
name = StringField()
|
||||
|
||||
MyDoc.drop_collection()
|
||||
|
||||
mydoc = MyDoc(name='testcase1', subs={'a': {'b': EmbeddedDoc(name='foo')}}).save()
|
||||
|
||||
mydoc = MyDoc.objects.first()
|
||||
subdoc = mydoc.subs['a']['b']
|
||||
subdoc.name = 'bar'
|
||||
|
||||
self.assertEqual(["name"], subdoc._get_changed_fields())
|
||||
self.assertEqual(["subs.a.b.name"], mydoc._get_changed_fields())
|
||||
|
||||
mydoc._clear_changed_fields()
|
||||
self.assertEqual([], mydoc._get_changed_fields())
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -490,6 +490,26 @@ class InstanceTest(unittest.TestCase):
|
||||
doc = Doc.objects.get()
|
||||
self.assertEqual(doc, doc.embedded_field[0]._instance)
|
||||
|
||||
def test_instance_is_set_on_setattr(self):
|
||||
|
||||
class Email(EmbeddedDocument):
|
||||
email = EmailField()
|
||||
def clean(self):
|
||||
print "instance:"
|
||||
print self._instance
|
||||
|
||||
class Account(Document):
|
||||
email = EmbeddedDocumentField(Email)
|
||||
|
||||
Account.drop_collection()
|
||||
acc = Account()
|
||||
acc.email = Email(email='test@example.com')
|
||||
self.assertTrue(hasattr(acc._data["email"], "_instance"))
|
||||
acc.save()
|
||||
|
||||
acc1 = Account.objects.first()
|
||||
self.assertTrue(hasattr(acc1._data["email"], "_instance"))
|
||||
|
||||
def test_document_clean(self):
|
||||
class TestDocument(Document):
|
||||
status = StringField()
|
||||
@@ -2392,6 +2412,24 @@ class InstanceTest(unittest.TestCase):
|
||||
system = System.objects.first()
|
||||
self.assertEqual("UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value)
|
||||
|
||||
def test_embedded_document_equality(self):
|
||||
|
||||
class Test(Document):
|
||||
field = StringField(required=True)
|
||||
|
||||
class Embedded(EmbeddedDocument):
|
||||
ref = ReferenceField(Test)
|
||||
|
||||
Test.drop_collection()
|
||||
test = Test(field='123').save() # has id
|
||||
|
||||
e = Embedded(ref=test)
|
||||
f1 = Embedded._from_son(e.to_mongo())
|
||||
f2 = Embedded._from_son(e.to_mongo())
|
||||
|
||||
self.assertEqual(f1, f2)
|
||||
f1.ref # Dereferences lazily
|
||||
self.assertEqual(f1, f2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -384,6 +384,9 @@ class FieldTest(unittest.TestCase):
|
||||
person.height = 4.0
|
||||
self.assertRaises(ValidationError, person.validate)
|
||||
|
||||
person_2 = Person(height='something invalid')
|
||||
self.assertRaises(ValidationError, person_2.validate)
|
||||
|
||||
def test_decimal_validation(self):
|
||||
"""Ensure that invalid values cannot be assigned to decimal fields.
|
||||
"""
|
||||
@@ -405,6 +408,11 @@ class FieldTest(unittest.TestCase):
|
||||
self.assertRaises(ValidationError, person.validate)
|
||||
person.height = Decimal('4.0')
|
||||
self.assertRaises(ValidationError, person.validate)
|
||||
person.height = 'something invalid'
|
||||
self.assertRaises(ValidationError, person.validate)
|
||||
|
||||
person_2 = Person(height='something invalid')
|
||||
self.assertRaises(ValidationError, person_2.validate)
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
@@ -1109,9 +1117,15 @@ class FieldTest(unittest.TestCase):
|
||||
post.info = {'$title': 'test'}
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
|
||||
post.info = {'nested': {'$title': 'test'}}
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
|
||||
post.info = {'the.title': 'test'}
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
|
||||
post.info = {'nested': {'the.title': 'test'}}
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
|
||||
post.info = {1: 'test'}
|
||||
self.assertRaises(ValidationError, post.validate)
|
||||
|
||||
@@ -1890,6 +1904,37 @@ class FieldTest(unittest.TestCase):
|
||||
Post.drop_collection()
|
||||
User.drop_collection()
|
||||
|
||||
def test_generic_reference_list_item_modification(self):
|
||||
"""Ensure that modifications of related documents (through generic reference) don't influence on querying
|
||||
"""
|
||||
class Post(Document):
|
||||
title = StringField()
|
||||
|
||||
class User(Document):
|
||||
username = StringField()
|
||||
bookmarks = ListField(GenericReferenceField())
|
||||
|
||||
Post.drop_collection()
|
||||
User.drop_collection()
|
||||
|
||||
post_1 = Post(title="Behind the Scenes of the Pavement Reunion")
|
||||
post_1.save()
|
||||
|
||||
user = User(bookmarks=[post_1])
|
||||
user.save()
|
||||
|
||||
post_1.title = "Title was modified"
|
||||
user.username = "New username"
|
||||
user.save()
|
||||
|
||||
user = User.objects(bookmarks__all=[post_1]).first()
|
||||
|
||||
self.assertNotEqual(user, None)
|
||||
self.assertEqual(user.bookmarks[0], post_1)
|
||||
|
||||
Post.drop_collection()
|
||||
User.drop_collection()
|
||||
|
||||
def test_binary_fields(self):
|
||||
"""Ensure that binary fields can be stored and retrieved.
|
||||
"""
|
||||
@@ -2546,6 +2591,20 @@ class FieldTest(unittest.TestCase):
|
||||
doc = Doc.objects.get()
|
||||
self.assertEqual(doc.embed_me.field_1, "hello")
|
||||
|
||||
def test_invalid_dict_value(self):
|
||||
class DictFieldTest(Document):
|
||||
dictionary = DictField(required=True)
|
||||
|
||||
DictFieldTest.drop_collection()
|
||||
|
||||
test = DictFieldTest(dictionary=None)
|
||||
test.dictionary # Just access to test getter
|
||||
self.assertRaises(ValidationError, test.validate)
|
||||
|
||||
test = DictFieldTest(dictionary=False)
|
||||
test.dictionary # Just access to test getter
|
||||
self.assertRaises(ValidationError, test.validate)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -279,7 +279,7 @@ class FileTest(unittest.TestCase):
|
||||
t.image.put(f)
|
||||
self.fail("Should have raised an invalidation error")
|
||||
except ValidationError, e:
|
||||
self.assertEquals("%s" % e, "Invalid image: cannot identify image file")
|
||||
self.assertEqual("%s" % e, "Invalid image: cannot identify image file")
|
||||
|
||||
t = TestImage()
|
||||
t.image.put(open(TEST_IMAGE_PATH, 'rb'))
|
||||
|
@@ -75,6 +75,12 @@ class GeoFieldTest(unittest.TestCase):
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
Location(loc=[1, 2]).validate()
|
||||
Location(loc={
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
81.4471435546875,
|
||||
23.61432859499169
|
||||
]}).validate()
|
||||
|
||||
def test_linestring_validation(self):
|
||||
class Location(Document):
|
||||
|
@@ -414,5 +414,47 @@ class GeoQueriesTest(unittest.TestCase):
|
||||
roads = Road.objects.filter(poly__geo_intersects={"$geometry": polygon}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
def test_2dsphere_point_sets_correctly(self):
|
||||
class Location(Document):
|
||||
loc = PointField()
|
||||
|
||||
Location.drop_collection()
|
||||
|
||||
Location(loc=[1,2]).save()
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["loc"], {"type": "Point", "coordinates": [1, 2]})
|
||||
|
||||
Location.objects.update(set__loc=[2,1])
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["loc"], {"type": "Point", "coordinates": [2, 1]})
|
||||
|
||||
def test_2dsphere_linestring_sets_correctly(self):
|
||||
class Location(Document):
|
||||
line = LineStringField()
|
||||
|
||||
Location.drop_collection()
|
||||
|
||||
Location(line=[[1, 2], [2, 2]]).save()
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["line"], {"type": "LineString", "coordinates": [[1, 2], [2, 2]]})
|
||||
|
||||
Location.objects.update(set__line=[[2, 1], [1, 2]])
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["line"], {"type": "LineString", "coordinates": [[2, 1], [1, 2]]})
|
||||
|
||||
def test_geojson_PolygonField(self):
|
||||
class Location(Document):
|
||||
poly = PolygonField()
|
||||
|
||||
Location.drop_collection()
|
||||
|
||||
Location(poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]).save()
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]})
|
||||
|
||||
Location.objects.update(set__poly=[[[40, 4], [40, 6], [41, 6], [40, 4]]])
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -2519,6 +2519,27 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
Product.drop_collection()
|
||||
|
||||
def test_distinct_ListField_EmbeddedDocumentField(self):
|
||||
|
||||
class Author(EmbeddedDocument):
|
||||
name = StringField()
|
||||
|
||||
class Book(Document):
|
||||
title = StringField()
|
||||
authors = ListField(EmbeddedDocumentField(Author))
|
||||
|
||||
Book.drop_collection()
|
||||
|
||||
mark_twain = Author(name="Mark Twain")
|
||||
john_tolkien = Author(name="John Ronald Reuel Tolkien")
|
||||
|
||||
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()
|
||||
authors = Book.objects.distinct("authors")
|
||||
|
||||
self.assertEqual(authors, [mark_twain, john_tolkien])
|
||||
|
||||
def test_custom_manager(self):
|
||||
"""Ensure that custom QuerySetManager instances work as expected.
|
||||
"""
|
||||
@@ -2861,6 +2882,19 @@ class QuerySetTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(10, Post.objects.limit(5).skip(5).count(with_limit_and_skip=False))
|
||||
|
||||
def test_count_and_none(self):
|
||||
"""Test count works with None()"""
|
||||
|
||||
class MyDoc(Document):
|
||||
pass
|
||||
|
||||
MyDoc.drop_collection()
|
||||
for i in xrange(0, 10):
|
||||
MyDoc().save()
|
||||
|
||||
self.assertEqual(MyDoc.objects.count(), 10)
|
||||
self.assertEqual(MyDoc.objects.none().count(), 0)
|
||||
|
||||
def test_call_after_limits_set(self):
|
||||
"""Ensure that re-filtering after slicing works
|
||||
"""
|
||||
@@ -3367,6 +3401,17 @@ class QuerySetTest(unittest.TestCase):
|
||||
self.assertEqual(1, MyDoc.objects.update_one(upsert=True, inc__47=1))
|
||||
self.assertEqual(MyDoc.objects.get()['47'], 1)
|
||||
|
||||
def test_dictfield_key_looks_like_a_digit(self):
|
||||
"""Only should work with DictField even if they have numeric keys."""
|
||||
|
||||
class MyDoc(Document):
|
||||
test = DictField()
|
||||
|
||||
MyDoc.drop_collection()
|
||||
doc = MyDoc(test={'47': 1})
|
||||
doc.save()
|
||||
self.assertEqual(MyDoc.objects.only('test__47').get().test['47'], 1)
|
||||
|
||||
def test_read_preference(self):
|
||||
class Bar(Document):
|
||||
pass
|
||||
@@ -3609,9 +3654,9 @@ class QuerySetTest(unittest.TestCase):
|
||||
docs = Noddy.objects.no_cache()
|
||||
|
||||
counter = len([1 for i in docs])
|
||||
self.assertEquals(counter, 100)
|
||||
self.assertEqual(counter, 100)
|
||||
|
||||
self.assertEquals(len(list(docs)), 100)
|
||||
self.assertEqual(len(list(docs)), 100)
|
||||
self.assertRaises(TypeError, lambda: len(docs))
|
||||
|
||||
with query_counter() as q:
|
||||
|
@@ -167,6 +167,35 @@ class TransformTest(unittest.TestCase):
|
||||
{'attachments.views.extracted': 'no'}]}
|
||||
self.assertEqual(expected, raw_query)
|
||||
|
||||
def test_geojson_PointField(self):
|
||||
class Location(Document):
|
||||
loc = PointField()
|
||||
|
||||
update = transform.update(Location, set__loc=[1, 2])
|
||||
self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}})
|
||||
|
||||
update = transform.update(Location, set__loc={"type": "Point", "coordinates": [1,2]})
|
||||
self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}})
|
||||
|
||||
def test_geojson_LineStringField(self):
|
||||
class Location(Document):
|
||||
line = LineStringField()
|
||||
|
||||
update = transform.update(Location, set__line=[[1, 2], [2, 2]])
|
||||
self.assertEqual(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}})
|
||||
|
||||
update = transform.update(Location, set__line={"type": "LineString", "coordinates": [[1, 2], [2, 2]]})
|
||||
self.assertEqual(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}})
|
||||
|
||||
def test_geojson_PolygonField(self):
|
||||
class Location(Document):
|
||||
poly = PolygonField()
|
||||
|
||||
update = transform.update(Location, set__poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]])
|
||||
self.assertEqual(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}})
|
||||
|
||||
update = transform.update(Location, set__poly={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]})
|
||||
self.assertEqual(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -98,6 +98,14 @@ class ConnectionTest(unittest.TestCase):
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'mongoenginetest2')
|
||||
|
||||
def test_register_connection_defaults(self):
|
||||
"""Ensure that defaults are used when the host and port are None.
|
||||
"""
|
||||
register_connection('testdb', 'mongoenginetest', host=None, port=None)
|
||||
|
||||
conn = get_connection('testdb')
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
def test_connection_kwargs(self):
|
||||
"""Ensure that connection kwargs get passed to pymongo.
|
||||
"""
|
||||
|
Reference in New Issue
Block a user