fixed merge conflict in BaseField.__init__

This commit is contained in:
blackbrrr 2010-01-12 12:19:30 -06:00
commit d85ee4e051
12 changed files with 497 additions and 34 deletions

View File

@ -27,6 +27,8 @@ Querying
.. autoclass:: mongoengine.queryset.QuerySet .. autoclass:: mongoengine.queryset.QuerySet
:members: :members:
.. automethod:: mongoengine.queryset.QuerySet.__call__
.. autofunction:: mongoengine.queryset.queryset_manager .. autofunction:: mongoengine.queryset.queryset_manager
Fields Fields

View File

@ -2,7 +2,22 @@
Changelog Changelog
========= =========
Changes is v0.1.3 Changes in v0.2.1
=================
- Added a MongoEngine backend for Django sessions
- Added force_insert to Document.save()
- Improved querying syntax for ListField and EmbeddedDocumentField
- Added support for user-defined primary keys (_ids in MongoDB)
Changes in v0.2
===============
- Added Q class for building advanced queries
- Added QuerySet methods for atomic updates to documents
- Fields may now specify ``unique=True`` to enforce uniqueness across a collection
- Added option for default document ordering
- Fixed bug in index definitions
Changes in v0.1.3
================= =================
- Added Django authentication backend - Added Django authentication backend
- Added Document.meta support for indexes, which are ensured just before - Added Document.meta support for indexes, which are ensured just before

View File

@ -27,3 +27,16 @@ file::
The :mod:`~mongoengine.django.auth` module also contains a The :mod:`~mongoengine.django.auth` module also contains a
:func:`~mongoengine.django.auth.get_user` helper function, that takes a user's :func:`~mongoengine.django.auth.get_user` helper function, that takes a user's
:attr:`id` and returns a :class:`~mongoengine.django.auth.User` object. :attr:`id` and returns a :class:`~mongoengine.django.auth.User` object.
Sessions
========
Django allows the use of different backend stores for its sessions. MongoEngine
provides a MongoDB-based session backend for Django, which allows you to use
sessions in you Django application with just MongoDB. To enable the MongoEngine
session backend, ensure that your settings module has
``'django.contrib.sessions.middleware.SessionMiddleware'`` in the
``MIDDLEWARE_CLASSES`` field and ``'django.contrib.sessions'`` in your
``INSTALLED_APPS``. From there, all you need to do is add the following line
into you settings module::
SESSION_ENGINE = 'mongoengine.django.sessions'

View File

@ -318,8 +318,25 @@ saved::
>>> page.id >>> page.id
ObjectId('123456789abcdef000000000') ObjectId('123456789abcdef000000000')
Alternatively, you may explicitly set the :attr:`id` before you save the Alternatively, you may define one of your own fields to be the document's
document, but the id must be a valid PyMongo :class:`ObjectId`. "primary key" by providing ``primary_key=True`` as a keyword argument to a
field's constructor. Under the hood, MongoEngine will use this field as the
:attr:`id`; in fact :attr:`id` is actually aliased to your primary key field so
you may still use :attr:`id` to access the primary key if you want::
>>> class User(Document):
... email = StringField(primary_key=True)
... name = StringField()
...
>>> bob = User(email='bob@example.com', name='Bob')
>>> bob.save()
>>> bob.id == bob.email == 'bob@example.com'
True
.. note::
If you define your own primary key field, the field implicitly becomes
required, so a :class:`ValidationError` will be thrown if you don't provide
it.
Querying the database Querying the database
===================== =====================
@ -454,6 +471,32 @@ would be generating "tag-clouds"::
from operator import itemgetter from operator import itemgetter
top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10] top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10]
Advanced queries
----------------
Sometimes calling a :class:`~mongoengine.queryset.QuerySet` object with keyword
arguments can't fully express the query you want to use -- for example if you
need to combine a number of constraints using *and* and *or*. This is made
possible in MongoEngine through the :class:`~mongoengine.queryset.Q` class.
A :class:`~mongoengine.queryset.Q` object represents part of a query, and
can be initialised using the same keyword-argument syntax you use to query
documents. To build a complex query, you may combine
:class:`~mongoengine.queryset.Q` objects using the ``&`` (and) and ``|`` (or)
operators. To use :class:`~mongoengine.queryset.Q` objects, pass them in
as positional arguments to :attr:`Document.objects` when you filter it by
calling it with keyword arguments::
# Get published posts
Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now()))
# Get top posts
Post.objects((Q(featured=True) & Q(hits__gte=1000)) | Q(hits__gte=5000))
.. warning::
Only use these advanced queries if absolutely necessary as they will execute
significantly slower than regular queries. This is because they are not
natively supported by MongoDB -- they are compiled to Javascript and sent
to the server for execution.
Atomic updates Atomic updates
-------------- --------------
Documents may be updated atomically by using the Documents may be updated atomically by using the

View File

@ -12,7 +12,7 @@ __all__ = (document.__all__ + fields.__all__ + connection.__all__ +
__author__ = 'Harry Marr' __author__ = 'Harry Marr'
VERSION = (0, 1, 3) VERSION = (0, 2, 1)
def get_version(): def get_version():
version = '%s.%s' % (VERSION[0], VERSION[1]) version = '%s.%s' % (VERSION[0], VERSION[1])

View File

@ -13,13 +13,13 @@ class BaseField(object):
""" """
def __init__(self, name=None, required=False, default=None, unique=False, def __init__(self, name=None, required=False, default=None, unique=False,
unique_with=None): unique_with=None, primary_key=False):
self.name = name self.name = name if not primary_key else '_id'
self.required = required self.required = required or primary_key
self.default = default self.default = default
self.unique = bool(unique or unique_with) self.unique = bool(unique or unique_with)
self.unique_with = unique_with self.unique_with = unique_with
self._loaded = [] self.primary_key = primary_key
def __get__(self, instance, owner): def __get__(self, instance, owner):
"""Descriptor for retrieving a value from a field in a document. Do """Descriptor for retrieving a value from a field in a document. Do
@ -73,7 +73,7 @@ class ObjectIdField(BaseField):
def to_mongo(self, value): def to_mongo(self, value):
if not isinstance(value, pymongo.objectid.ObjectId): if not isinstance(value, pymongo.objectid.ObjectId):
return pymongo.objectid.ObjectId(value) return pymongo.objectid.ObjectId(str(value))
return value return value
def prepare_query_value(self, value): def prepare_query_value(self, value):
@ -140,6 +140,8 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
collection = name.lower() collection = name.lower()
simple_class = True simple_class = True
id_field = None
base_indexes = []
# Subclassed documents inherit collection from superclass # Subclassed documents inherit collection from superclass
for base in bases: for base in bases:
@ -154,17 +156,23 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
simple_class = False simple_class = False
collection = base._meta['collection'] collection = base._meta['collection']
id_field = id_field or base._meta.get('id_field')
base_indexes += base._meta.get('indexes', [])
meta = { meta = {
'collection': collection, 'collection': collection,
'allow_inheritance': True, 'allow_inheritance': True,
'max_documents': None, 'max_documents': None,
'max_size': None, 'max_size': None,
'ordering': [], # default ordering applied at runtime 'ordering': [], # default ordering applied at runtime
'indexes': [] # indexes to be ensured at runtime 'indexes': [], # indexes to be ensured at runtime
'id_field': id_field,
} }
# Apply document-defined meta options # Apply document-defined meta options
meta.update(attrs.get('meta', {})) meta.update(attrs.get('meta', {}))
meta['indexes'] += base_indexes
# Only simple classes - direct subclasses of Document - may set # Only simple classes - direct subclasses of Document - may set
# allow_inheritance to False # allow_inheritance to False
@ -173,16 +181,14 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
'"allow_inheritance" to False') '"allow_inheritance" to False')
attrs['_meta'] = meta attrs['_meta'] = meta
attrs['id'] = ObjectIdField(name='_id')
# Set up collection manager, needs the class to have fields so use # Set up collection manager, needs the class to have fields so use
# DocumentMetaclass before instantiating CollectionManager object # DocumentMetaclass before instantiating CollectionManager object
new_class = super_new(cls, name, bases, attrs) new_class = super_new(cls, name, bases, attrs)
new_class.objects = QuerySetManager() new_class.objects = QuerySetManager()
# Generate a list of indexes needed by uniqueness constraints
unique_indexes = [] unique_indexes = []
for field_name, field in new_class._fields.items(): for field_name, field in new_class._fields.items():
# Generate a list of indexes needed by uniqueness constraints
if field.unique: if field.unique:
field.required = True field.required = True
unique_fields = [field_name] unique_fields = [field_name]
@ -205,10 +211,25 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
unique_fields += unique_with unique_fields += unique_with
# Add the new index to the list # Add the new index to the list
index = [(field, pymongo.ASCENDING) for field in unique_fields] index = [(f, pymongo.ASCENDING) for f in unique_fields]
unique_indexes.append(index) unique_indexes.append(index)
# Check for custom primary key
if field.primary_key:
if not new_class._meta['id_field']:
new_class._meta['id_field'] = field_name
# Make 'Document.id' an alias to the real primary key field
new_class.id = field
#new_class._fields['id'] = field
else:
raise ValueError('Cannot override primary key field')
new_class._meta['unique_indexes'] = unique_indexes new_class._meta['unique_indexes'] = unique_indexes
if not new_class._meta['id_field']:
new_class._meta['id_field'] = 'id'
new_class.id = new_class._fields['id'] = ObjectIdField(name='_id')
return new_class return new_class
@ -271,6 +292,18 @@ class BaseDocument(object):
def __len__(self): def __len__(self):
return len(self._data) return len(self._data)
def __repr__(self):
try:
u = unicode(self)
except (UnicodeEncodeError, UnicodeDecodeError):
u = '[Bad Unicode data]'
return u'<%s: %s>' % (self.__class__.__name__, u)
def __str__(self):
if hasattr(self, '__unicode__'):
return unicode(self).encode('utf-8')
return '%s object' % self.__class__.__name__
def to_mongo(self): def to_mongo(self):
"""Return data dictionary ready for use with MongoDB. """Return data dictionary ready for use with MongoDB.
""" """

View File

@ -0,0 +1,63 @@
from django.contrib.sessions.backends.base import SessionBase, CreateError
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_unicode
from mongoengine.document import Document
from mongoengine import fields
from mongoengine.queryset import OperationError
from datetime import datetime
class MongoSession(Document):
session_key = fields.StringField(primary_key=True, max_length=40)
session_data = fields.StringField()
expire_date = fields.DateTimeField()
meta = {'collection': 'django_session', 'allow_inheritance': False}
class SessionStore(SessionBase):
"""A MongoEngine-based session store for Django.
"""
def load(self):
try:
s = MongoSession.objects(session_key=self.session_key,
expire_date__gt=datetime.now())[0]
return self.decode(force_unicode(s.session_data))
except (IndexError, SuspiciousOperation):
self.create()
return {}
def exists(self, session_key):
return bool(MongoSession.objects(session_key=session_key).first())
def create(self):
while True:
self.session_key = self._get_new_session_key()
try:
self.save(must_create=True)
except CreateError:
continue
self.modified = True
self._session_cache = {}
return
def save(self, must_create=False):
s = MongoSession(session_key=self.session_key)
s.session_data = self.encode(self._get_session(no_load=must_create))
s.expire_date = self.get_expiry_date()
try:
s.save(force_insert=must_create, safe=True)
except OperationError:
if must_create:
raise CreateError
raise
def delete(self, session_key=None):
if session_key is None:
if self.session_key is None:
return
session_key = self.session_key
MongoSession.objects(session_key=session_key).delete()

View File

@ -56,31 +56,54 @@ class Document(BaseDocument):
__metaclass__ = TopLevelDocumentMetaclass __metaclass__ = TopLevelDocumentMetaclass
def save(self, safe=True): def save(self, safe=True, force_insert=False):
"""Save the :class:`~mongoengine.Document` to the database. If the """Save the :class:`~mongoengine.Document` to the database. If the
document already exists, it will be updated, otherwise it will be document already exists, it will be updated, otherwise it will be
created. created.
If ``safe=True`` and the operation is unsuccessful, an
:class:`~mongoengine.OperationError` will be raised.
:param safe: check if the operation succeeded before returning
:param force_insert: only try to create a new document, don't allow
updates of existing documents
""" """
self.validate() self.validate()
doc = self.to_mongo() doc = self.to_mongo()
try: try:
object_id = self.__class__.objects._collection.save(doc, safe=safe) collection = self.__class__.objects._collection
if force_insert:
object_id = collection.insert(doc, safe=safe)
else:
object_id = collection.save(doc, safe=safe)
except pymongo.errors.OperationFailure, err: except pymongo.errors.OperationFailure, err:
raise OperationError('Tried to save duplicate unique keys (%s)' message = 'Could not save document (%s)'
% str(err)) if 'duplicate key' in str(err):
self.id = self._fields['id'].to_python(object_id) message = 'Tried to save duplicate unique keys (%s)'
raise OperationError(message % str(err))
id_field = self._meta['id_field']
self[id_field] = self._fields[id_field].to_python(object_id)
def delete(self): def delete(self, safe=False):
"""Delete the :class:`~mongoengine.Document` from the database. This """Delete the :class:`~mongoengine.Document` from the database. This
will only take effect if the document has been previously saved. will only take effect if the document has been previously saved.
:param safe: check if the operation succeeded before returning
""" """
object_id = self._fields['id'].to_mongo(self.id) id_field = self._meta['id_field']
self.__class__.objects(id=object_id).delete() object_id = self._fields[id_field].to_mongo(self[id_field])
try:
self.__class__.objects(**{id_field: object_id}).delete(safe=safe)
except pymongo.errors.OperationFailure, err:
raise OperationError('Could not delete document (%s)' % str(err))
def reload(self): def reload(self):
"""Reloads all attributes from the database. """Reloads all attributes from the database.
.. versionadded:: 0.1.2
""" """
obj = self.__class__.objects(id=self.id).first() id_field = self._meta['id_field']
obj = self.__class__.objects(**{id_field: self[id_field]}).first()
for field in self._fields: for field in self._fields:
setattr(self, field, obj[field]) setattr(self, field, obj[field])

View File

@ -82,6 +82,8 @@ class FloatField(BaseField):
class BooleanField(BaseField): class BooleanField(BaseField):
"""A boolean field type. """A boolean field type.
.. versionadded:: 0.1.2
""" """
def to_python(self, value): def to_python(self, value):
@ -131,6 +133,9 @@ class EmbeddedDocumentField(BaseField):
def lookup_member(self, member_name): def lookup_member(self, member_name):
return self.document._fields.get(member_name) return self.document._fields.get(member_name)
def prepare_query_value(self, value):
return self.to_mongo(value)
class ListField(BaseField): class ListField(BaseField):
"""A list field that wraps a standard field, allowing multiple instances """A list field that wraps a standard field, allowing multiple instances
@ -163,6 +168,9 @@ class ListField(BaseField):
raise ValidationError('All items in a list field must be of the ' raise ValidationError('All items in a list field must be of the '
'specified type') 'specified type')
def prepare_query_value(self, value):
return self.field.to_mongo(value)
def lookup_member(self, member_name): def lookup_member(self, member_name):
return self.field.lookup_member(member_name) return self.field.lookup_member(member_name)

View File

@ -1,9 +1,14 @@
from connection import _get_db from connection import _get_db
import pymongo import pymongo
import copy
__all__ = ['queryset_manager', 'InvalidQueryError', 'InvalidCollectionError'] __all__ = ['queryset_manager', 'Q', 'InvalidQueryError',
'InvalidCollectionError']
# The maximum number of items to display in a QuerySet.__repr__
REPR_OUTPUT_SIZE = 20
class InvalidQueryError(Exception): class InvalidQueryError(Exception):
@ -14,6 +19,88 @@ class OperationError(Exception):
pass pass
class Q(object):
OR = '||'
AND = '&&'
OPERATORS = {
'eq': 'this.%(field)s == %(value)s',
'neq': 'this.%(field)s != %(value)s',
'gt': 'this.%(field)s > %(value)s',
'gte': 'this.%(field)s >= %(value)s',
'lt': 'this.%(field)s < %(value)s',
'lte': 'this.%(field)s <= %(value)s',
'lte': 'this.%(field)s <= %(value)s',
'in': 'this.%(field)s.indexOf(%(value)s) != -1',
'nin': 'this.%(field)s.indexOf(%(value)s) == -1',
'mod': '%(field)s %% %(value)s',
'all': ('%(value)s.every(function(a){'
'return this.%(field)s.indexOf(a) != -1 })'),
'size': 'this.%(field)s.length == %(value)s',
'exists': 'this.%(field)s != null',
}
def __init__(self, **query):
self.query = [query]
def _combine(self, other, op):
obj = Q()
obj.query = ['('] + copy.deepcopy(self.query) + [op]
obj.query += copy.deepcopy(other.query) + [')']
return obj
def __or__(self, other):
return self._combine(other, self.OR)
def __and__(self, other):
return self._combine(other, self.AND)
def as_js(self, document):
js = []
js_scope = {}
for i, item in enumerate(self.query):
if isinstance(item, dict):
item_query = QuerySet._transform_query(document, **item)
# item_query will values will either be a value or a dict
js.append(self._item_query_as_js(item_query, js_scope, i))
else:
js.append(item)
return pymongo.code.Code(' '.join(js), js_scope)
def _item_query_as_js(self, item_query, js_scope, item_num):
# item_query will be in one of the following forms
# {'age': 25, 'name': 'Test'}
# {'age': {'$lt': 25}, 'name': {'$in': ['Test', 'Example']}
# {'age': {'$lt': 25, '$gt': 18}}
js = []
for i, (key, value) in enumerate(item_query.items()):
op = 'eq'
# Construct a variable name for the value in the JS
value_name = 'i%sf%s' % (item_num, i)
if isinstance(value, dict):
# Multiple operators for this field
for j, (op, value) in enumerate(value.items()):
# Create a custom variable name for this operator
op_value_name = '%so%s' % (value_name, j)
# Update the js scope with the value for this op
js_scope[op_value_name] = value
# Construct the JS that uses this op
operation_js = Q.OPERATORS[op.strip('$')] % {
'field': key,
'value': op_value_name
}
js.append(operation_js)
else:
js_scope[value_name] = value
# Construct the JS for this field
field_js = Q.OPERATORS[op.strip('$')] % {
'field': key,
'value': value_name
}
js.append(field_js)
return ' && '.join(js)
class QuerySet(object): class QuerySet(object):
"""A set of results returned from a query. Wraps a MongoDB cursor, """A set of results returned from a query. Wraps a MongoDB cursor,
providing :class:`~mongoengine.Document` objects as the results. providing :class:`~mongoengine.Document` objects as the results.
@ -24,6 +111,7 @@ class QuerySet(object):
self._collection_obj = collection self._collection_obj = collection
self._accessed_collection = False self._accessed_collection = False
self._query = {} self._query = {}
self._where_clauses = []
# If inheritance is allowed, only return instances and instances of # If inheritance is allowed, only return instances and instances of
# subclasses of the class being used # subclasses of the class being used
@ -33,6 +121,10 @@ class QuerySet(object):
def ensure_index(self, key_or_list): def ensure_index(self, key_or_list):
"""Ensure that the given indexes are in place. """Ensure that the given indexes are in place.
:param key_or_list: a single index key or a list of index keys (to
construct a multi-field index); keys may be prefixed with a **+**
or a **-** to determine the index ordering
""" """
if isinstance(key_or_list, basestring): if isinstance(key_or_list, basestring):
key_or_list = [key_or_list] key_or_list = [key_or_list]
@ -55,10 +147,15 @@ class QuerySet(object):
self._collection.ensure_index(index_list) self._collection.ensure_index(index_list)
return self return self
def __call__(self, **query): def __call__(self, *q_objs, **query):
"""Filter the selected documents by calling the """Filter the selected documents by calling the
:class:`~mongoengine.QuerySet` with a query. :class:`~mongoengine.QuerySet` with a query.
:param q_objs: :class:`~mongoengine.Q` objects to be used in the query
:param query: Django-style query keyword arguments
""" """
for q in q_objs:
self._where_clauses.append(q.as_js(self._document))
query = QuerySet._transform_query(_doc_cls=self._document, **query) query = QuerySet._transform_query(_doc_cls=self._document, **query)
self._query.update(query) self._query.update(query)
return self return self
@ -92,6 +189,9 @@ class QuerySet(object):
def _cursor(self): def _cursor(self):
if not self._cursor_obj: if not self._cursor_obj:
self._cursor_obj = self._collection.find(self._query) self._cursor_obj = self._collection.find(self._query)
# Apply where clauses to cursor
for js in self._where_clauses:
self._cursor_obj.where(js)
# apply default ordering # apply default ordering
if self._document._meta['ordering']: if self._document._meta['ordering']:
@ -179,11 +279,13 @@ class QuerySet(object):
def with_id(self, object_id): def with_id(self, object_id):
"""Retrieve the object matching the id provided. """Retrieve the object matching the id provided.
"""
if not isinstance(object_id, pymongo.objectid.ObjectId):
object_id = pymongo.objectid.ObjectId(str(object_id))
result = self._collection.find_one(object_id) :param object_id: the value for the id of the document to look up
"""
id_field = self._document._meta['id_field']
object_id = self._document._fields[id_field].to_mongo(object_id)
result = self._collection.find_one({'_id': object_id})
if result is not None: if result is not None:
result = self._document._from_son(result) result = self._document._from_son(result)
return result return result
@ -204,6 +306,8 @@ class QuerySet(object):
def limit(self, n): def limit(self, n):
"""Limit the number of returned documents to `n`. This may also be """Limit the number of returned documents to `n`. This may also be
achieved using array-slicing syntax (e.g. ``User.objects[:5]``). achieved using array-slicing syntax (e.g. ``User.objects[:5]``).
:param n: the maximum number of objects to return
""" """
self._cursor.limit(n) self._cursor.limit(n)
# Return self to allow chaining # Return self to allow chaining
@ -212,6 +316,8 @@ class QuerySet(object):
def skip(self, n): def skip(self, n):
"""Skip `n` documents before returning the results. This may also be """Skip `n` documents before returning the results. This may also be
achieved using array-slicing syntax (e.g. ``User.objects[5:]``). achieved using array-slicing syntax (e.g. ``User.objects[5:]``).
:param n: the number of objects to skip before returning results
""" """
self._cursor.skip(n) self._cursor.skip(n)
return self return self
@ -232,6 +338,9 @@ class QuerySet(object):
"""Order the :class:`~mongoengine.queryset.QuerySet` by the keys. The """Order the :class:`~mongoengine.queryset.QuerySet` by the keys. The
order may be specified by prepending each of the keys by a + or a -. order may be specified by prepending each of the keys by a + or a -.
Ascending order is assumed. Ascending order is assumed.
:param keys: fields to order the query results by; keys may be
prefixed with **+** or **-** to determine the ordering direction
""" """
key_list = [] key_list = []
for key in keys: for key in keys:
@ -248,6 +357,8 @@ class QuerySet(object):
def explain(self, format=False): def explain(self, format=False):
"""Return an explain plan record for the """Return an explain plan record for the
:class:`~mongoengine.queryset.QuerySet`\ 's cursor. :class:`~mongoengine.queryset.QuerySet`\ 's cursor.
:param format: format the plan before returning it
""" """
plan = self._cursor.explain() plan = self._cursor.explain()
@ -256,10 +367,12 @@ class QuerySet(object):
plan = pprint.pformat(plan) plan = pprint.pformat(plan)
return plan return plan
def delete(self): def delete(self, safe=False):
"""Delete the documents matched by the query. """Delete the documents matched by the query.
:param safe: check if the operation succeeded before returning
""" """
self._collection.remove(self._query) self._collection.remove(self._query, safe=safe)
@classmethod @classmethod
def _transform_update(cls, _doc_cls=None, **update): def _transform_update(cls, _doc_cls=None, **update):
@ -312,6 +425,11 @@ class QuerySet(object):
def update(self, safe_update=True, **update): def update(self, safe_update=True, **update):
"""Perform an atomic update on the fields matched by the query. """Perform an atomic update on the fields matched by the query.
:param safe: check if the operation succeeded before returning
:param update: Django-style update keyword arguments
.. versionadded:: 0.2
""" """
if pymongo.version < '1.1.1': if pymongo.version < '1.1.1':
raise OperationError('update() method requires PyMongo 1.1.1+') raise OperationError('update() method requires PyMongo 1.1.1+')
@ -327,6 +445,11 @@ class QuerySet(object):
def update_one(self, safe_update=True, **update): def update_one(self, safe_update=True, **update):
"""Perform an atomic update on first field matched by the query. """Perform an atomic update on first field matched by the query.
:param safe: check if the operation succeeded before returning
:param update: Django-style update keyword arguments
.. versionadded:: 0.2
""" """
update = QuerySet._transform_update(self._document, **update) update = QuerySet._transform_update(self._document, **update)
try: try:
@ -352,6 +475,12 @@ class QuerySet(object):
collection in use; ``query``, which is an object representing the collection in use; ``query``, which is an object representing the
current query; and ``options``, which is an object containing any current query; and ``options``, which is an object containing any
options specified as keyword arguments. options specified as keyword arguments.
:param code: a string of Javascript code to execute
:param fields: fields that you will be using in your function, which
will be passed in to your function as arguments
:param options: options that you want available to the function
(accessed in Javascript through the ``options`` object)
""" """
fields = [QuerySet._translate_field_name(self._document, f) fields = [QuerySet._translate_field_name(self._document, f)
for f in fields] for f in fields]
@ -368,6 +497,9 @@ class QuerySet(object):
def sum(self, field): def sum(self, field):
"""Sum over the values of the specified field. """Sum over the values of the specified field.
:param field: the field to sum over; use dot-notation to refer to
embedded document fields
""" """
sum_func = """ sum_func = """
function(sumField) { function(sumField) {
@ -382,6 +514,9 @@ class QuerySet(object):
def average(self, field): def average(self, field):
"""Average over the values of the specified field. """Average over the values of the specified field.
:param field: the field to average over; use dot-notation to refer to
embedded document fields
""" """
average_func = """ average_func = """
function(averageField) { function(averageField) {
@ -402,6 +537,9 @@ class QuerySet(object):
"""Returns a dictionary of all items present in a list field across """Returns a dictionary of all items present in a list field across
the whole queried set of documents, and their corresponding frequency. the whole queried set of documents, and their corresponding frequency.
This is useful for generating tag clouds, or searching documents. This is useful for generating tag clouds, or searching documents.
:param list_field: the list field to use
:param normalize: normalize the results so they add to 1.0
""" """
freq_func = """ freq_func = """
function(listField) { function(listField) {
@ -427,6 +565,11 @@ class QuerySet(object):
""" """
return self.exec_js(freq_func, list_field, normalize=normalize) return self.exec_js(freq_func, list_field, normalize=normalize)
def __repr__(self):
data = list(self[:REPR_OUTPUT_SIZE + 1])
if len(data) > REPR_OUTPUT_SIZE:
data[-1] = "...(remaining elements truncated)..."
return repr(data)
class InvalidCollectionError(Exception): class InvalidCollectionError(Exception):
pass pass

View File

@ -245,6 +245,19 @@ class DocumentTest(unittest.TestCase):
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)] self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
in info.values()) in info.values())
self.assertTrue([('_types', 1), ('addDate', -1)] in info.values()) self.assertTrue([('_types', 1), ('addDate', -1)] in info.values())
class ExtendedBlogPost(BlogPost):
title = StringField()
meta = {'indexes': ['title']}
BlogPost.drop_collection()
list(ExtendedBlogPost.objects)
info = ExtendedBlogPost.objects._collection.index_information()
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
in info.values())
self.assertTrue([('_types', 1), ('addDate', -1)] in info.values())
self.assertTrue([('_types', 1), ('title', 1)] in info.values())
BlogPost.drop_collection() BlogPost.drop_collection()
@ -287,6 +300,39 @@ class DocumentTest(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
def test_custom_id_field(self):
"""Ensure that documents may be created with custom primary keys.
"""
class User(Document):
username = StringField(primary_key=True)
name = StringField()
User.drop_collection()
self.assertEqual(User._fields['username'].name, '_id')
self.assertEqual(User._meta['id_field'], 'username')
def create_invalid_user():
User(name='test').save() # no primary key field
self.assertRaises(ValidationError, create_invalid_user)
def define_invalid_user():
class EmailUser(User):
email = StringField(primary_key=True)
self.assertRaises(ValueError, define_invalid_user)
user = User(username='test', name='test user')
user.save()
user_obj = User.objects.first()
self.assertEqual(user_obj.id, 'test')
user_son = User.objects._collection.find_one()
self.assertEqual(user_son['_id'], 'test')
self.assertTrue('username' not in user_son['_id'])
User.drop_collection()
def test_creation(self): def test_creation(self):
"""Ensure that document may be created using keyword arguments. """Ensure that document may be created using keyword arguments.
""" """

View File

@ -1,5 +1,6 @@
import unittest import unittest
import pymongo import pymongo
from datetime import datetime
from mongoengine.queryset import QuerySet from mongoengine.queryset import QuerySet
from mongoengine import * from mongoengine import *
@ -16,7 +17,7 @@ class QuerySetTest(unittest.TestCase):
self.Person = Person self.Person = Person
def test_initialisation(self): def test_initialisation(self):
"""Ensure that CollectionManager is correctly initialised. """Ensure that a QuerySet is correctly initialised by QuerySetManager.
""" """
self.assertTrue(isinstance(self.Person.objects, QuerySet)) self.assertTrue(isinstance(self.Person.objects, QuerySet))
self.assertEqual(self.Person.objects._collection.name(), self.assertEqual(self.Person.objects._collection.name(),
@ -48,6 +49,9 @@ class QuerySetTest(unittest.TestCase):
person2 = self.Person(name="User B", age=30) person2 = self.Person(name="User B", age=30)
person2.save() person2.save()
q1 = Q(name='test')
q2 = Q(age__gte=18)
# Find all people in the collection # Find all people in the collection
people = self.Person.objects people = self.Person.objects
self.assertEqual(len(people), 2) self.assertEqual(len(people), 2)
@ -170,8 +174,6 @@ class QuerySetTest(unittest.TestCase):
def test_ordering(self): def test_ordering(self):
"""Ensure default ordering is applied and can be overridden. """Ensure default ordering is applied and can be overridden.
""" """
from datetime import datetime
class BlogPost(Document): class BlogPost(Document):
title = StringField() title = StringField()
published_date = DateTimeField() published_date = DateTimeField()
@ -180,6 +182,8 @@ class QuerySetTest(unittest.TestCase):
'ordering': ['-published_date'] 'ordering': ['-published_date']
} }
BlogPost.drop_collection()
blog_post_1 = BlogPost(title="Blog Post #1", blog_post_1 = BlogPost(title="Blog Post #1",
published_date=datetime(2010, 1, 5, 0, 0 ,0)) published_date=datetime(2010, 1, 5, 0, 0 ,0))
blog_post_2 = BlogPost(title="Blog Post #2", blog_post_2 = BlogPost(title="Blog Post #2",
@ -212,6 +216,8 @@ class QuerySetTest(unittest.TestCase):
content = StringField() content = StringField()
author = EmbeddedDocumentField(User) author = EmbeddedDocumentField(User)
BlogPost.drop_collection()
post = BlogPost(content='Had a good coffee today...') post = BlogPost(content='Had a good coffee today...')
post.author = User(name='Test User') post.author = User(name='Test User')
post.save() post.save()
@ -222,6 +228,42 @@ class QuerySetTest(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
def test_q(self):
class BlogPost(Document):
publish_date = DateTimeField()
published = BooleanField()
BlogPost.drop_collection()
post1 = BlogPost(publish_date=datetime(2010, 1, 8), published=False)
post1.save()
post2 = BlogPost(publish_date=datetime(2010, 1, 15), published=True)
post2.save()
post3 = BlogPost(published=True)
post3.save()
post4 = BlogPost(publish_date=datetime(2010, 1, 8))
post4.save()
post5 = BlogPost(publish_date=datetime(2010, 1, 15))
post5.save()
post6 = BlogPost(published=False)
post6.save()
date = datetime(2010, 1, 10)
q = BlogPost.objects(Q(publish_date__lte=date) | Q(published=True))
posts = [post.id for post in q]
published_posts = (post1, post2, post3, post4)
self.assertTrue(all(obj.id in posts for obj in published_posts))
self.assertFalse(any(obj.id in posts for obj in [post5, post6]))
BlogPost.drop_collection()
def test_delete(self): def test_delete(self):
"""Ensure that documents are properly deleted from the database. """Ensure that documents are properly deleted from the database.
""" """
@ -464,5 +506,37 @@ class QuerySetTest(unittest.TestCase):
self.Person.drop_collection() self.Person.drop_collection()
class QTest(unittest.TestCase):
def test_or_and(self):
q1 = Q(name='test')
q2 = Q(age__gte=18)
query = ['(', {'name': 'test'}, '||', {'age__gte': 18}, ')']
self.assertEqual((q1 | q2).query, query)
query = ['(', {'name': 'test'}, '&&', {'age__gte': 18}, ')']
self.assertEqual((q1 & q2).query, query)
query = ['(', '(', {'name': 'test'}, '&&', {'age__gte': 18}, ')', '||',
{'name': 'example'}, ')']
self.assertEqual((q1 & q2 | Q(name='example')).query, query)
def test_item_query_as_js(self):
"""Ensure that the _item_query_as_js utilitiy method works properly.
"""
q = Q()
examples = [
({'name': 'test'}, 'this.name == i0f0', {'i0f0': 'test'}),
({'age': {'$gt': 18}}, 'this.age > i0f0o0', {'i0f0o0': 18}),
({'name': 'test', 'age': {'$gt': 18, '$lte': 65}},
'this.age <= i0f0o0 && this.age > i0f0o1 && this.name == i0f1',
{'i0f0o0': 65, 'i0f0o1': 18, 'i0f1': 'test'}),
]
for item, js, scope in examples:
test_scope = {}
self.assertEqual(q._item_query_as_js(item, test_scope, 0), js)
self.assertEqual(scope, test_scope)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()