diff --git a/AUTHORS b/AUTHORS index af23070b..93ecfa8d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,4 @@ -Harry Marr +Harry Marr Matt Dennewitz -Deepak Thukral \ No newline at end of file +Deepak Thukral +Florian Schlachter diff --git a/docs/apireference.rst b/docs/apireference.rst index ec78efa0..2433ddfe 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -54,6 +54,8 @@ Fields .. autoclass:: mongoengine.ListField +.. autoclass:: mongoengine.BinaryField + .. autoclass:: mongoengine.ObjectIdField .. autoclass:: mongoengine.ReferenceField diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 6b14b12e..ddab385e 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -13,6 +13,13 @@ fetch documents from the database:: for user in User.objects: print user.name +.. note:: + Once the iteration finishes (when :class:`StopIteration` is raised), + :meth:`~mongoengine.queryset.QuerySet.rewind` will be called so that the + :class:`~mongoengine.queryset.QuerySet` may be iterated over again. The + results of the first iteration are *not* cached, so the database will be hit + each time the :class:`~mongoengine.queryset.QuerySet` is iterated over. + Filtering queries ================= The query may be filtered by calling the diff --git a/mongoengine/base.py b/mongoengine/base.py index 1921cba7..e0127537 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -86,7 +86,8 @@ class ObjectIdField(BaseField): try: return pymongo.objectid.ObjectId(str(value)) except Exception, e: - raise ValidationError(e.message) + #e.message attribute has been deprecated since Python 2.6 + raise ValidationError(str(e)) return value def prepare_query_value(self, op, value): diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py index 3c76baf2..d4b0ff0b 100644 --- a/mongoengine/django/auth.py +++ b/mongoengine/django/auth.py @@ -30,6 +30,7 @@ class User(Document): is_active = BooleanField(default=True) is_superuser = BooleanField(default=False) last_login = DateTimeField(default=datetime.datetime.now) + date_joined = DateTimeField(default=datetime.datetime.now) def get_full_name(self): """Returns the users first and last names, separated by a space. @@ -70,7 +71,20 @@ class User(Document): """Create (and save) a new user with the given username, password and email address. """ - user = User(username=username, email=email) + now = datetime.datetime.now() + + # Normalize the address by lowercasing the domain part of the email + # address. + # Not sure why we'r allowing null email when its not allowed in django + if email is not None: + try: + email_name, domain_part = email.strip().split('@', 1) + except ValueError: + pass + else: + email = '@'.join([email_name, domain_part.lower()]) + + user = User(username=username, email=email, date_joined=now) user.set_password(password) user.save() return user diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 0793f44b..fcae63ee 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -11,7 +11,8 @@ import decimal __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField', 'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField', 'ObjectIdField', 'ReferenceField', 'ValidationError', - 'DecimalField', 'URLField', 'GenericReferenceField'] + 'DecimalField', 'URLField', 'GenericReferenceField', + 'BinaryField'] RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -442,3 +443,23 @@ class GenericReferenceField(BaseField): def prepare_query_value(self, op, value): return self.to_mongo(value)['_ref'] + +class BinaryField(BaseField): + """A binary data field. + """ + + def __init__(self, max_bytes=None, **kwargs): + self.max_bytes = max_bytes + super(BinaryField, self).__init__(**kwargs) + + def to_mongo(self, value): + return pymongo.binary.Binary(value) + + def to_python(self, value): + return str(value) + + def validate(self, value): + assert isinstance(value, str) + + if self.max_bytes is not None and len(value) > self.max_bytes: + raise ValidationError('Binary value is too long') diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 883154f2..6a397b3f 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -418,9 +418,18 @@ class QuerySet(object): def next(self): """Wrap the result in a :class:`~mongoengine.Document` object. """ - if self._limit == 0: - raise StopIteration - return self._document._from_son(self._cursor.next()) + try: + if self._limit == 0: + raise StopIteration + return self._document._from_son(self._cursor.next()) + except StopIteration, e: + self.rewind() + raise e + + def rewind(self): + """Rewind the cursor to its unevaluated state. + """ + self._cursor.rewind() def count(self): """Count the selected elements in the query. diff --git a/tests/fields.py b/tests/fields.py index 94f65186..986f0e26 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -495,6 +495,61 @@ class FieldTest(unittest.TestCase): Post.drop_collection() User.drop_collection() + def test_binary_fields(self): + """Ensure that binary fields can be stored and retrieved. + """ + class Attachment(Document): + content_type = StringField() + blob = BinaryField() + + BLOB = '\xe6\x00\xc4\xff\x07' + MIME_TYPE = 'application/octet-stream' + + Attachment.drop_collection() + + attachment = Attachment(content_type=MIME_TYPE, blob=BLOB) + attachment.save() + + attachment_1 = Attachment.objects().first() + self.assertEqual(MIME_TYPE, attachment_1.content_type) + self.assertEqual(BLOB, attachment_1.blob) + + Attachment.drop_collection() + + def test_binary_validation(self): + """Ensure that invalid values cannot be assigned to binary fields. + """ + class Attachment(Document): + blob = BinaryField() + + class AttachmentRequired(Document): + blob = BinaryField(required=True) + + class AttachmentSizeLimit(Document): + blob = BinaryField(max_bytes=4) + + Attachment.drop_collection() + AttachmentRequired.drop_collection() + AttachmentSizeLimit.drop_collection() + + attachment = Attachment() + attachment.validate() + attachment.blob = 2 + self.assertRaises(ValidationError, attachment.validate) + + attachment_required = AttachmentRequired() + self.assertRaises(ValidationError, attachment_required.validate) + attachment_required.blob = '\xe6\x00\xc4\xff\x07' + attachment_required.validate() + + attachment_size_limit = AttachmentSizeLimit(blob='\xe6\x00\xc4\xff\x07') + self.assertRaises(ValidationError, attachment_size_limit.validate) + attachment_size_limit.blob = '\xe6\x00\xc4\xff' + attachment_size_limit.validate() + + Attachment.drop_collection() + AttachmentRequired.drop_collection() + AttachmentSizeLimit.drop_collection() if __name__ == '__main__': unittest.main() diff --git a/tests/queryset.py b/tests/queryset.py index c6454738..25dcb237 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -189,6 +189,18 @@ class QuerySetTest(unittest.TestCase): person = self.Person.objects.get(age=50) self.assertEqual(person.name, "User C") + def test_repeated_iteration(self): + """Ensure that QuerySet rewinds itself one iteration finishes. + """ + self.Person(name='Person 1').save() + self.Person(name='Person 2').save() + + queryset = self.Person.objects + people1 = [person for person in queryset] + people2 = [person for person in queryset] + + self.assertEqual(people1, people2) + def test_regex_query_shortcuts(self): """Ensure that contains, startswith, endswith, etc work. """