From 1818cf711417c17467c8adb186b102fbedbd363e Mon Sep 17 00:00:00 2001 From: Harry Marr Date: Tue, 12 Jan 2010 18:33:33 +0000 Subject: [PATCH 1/5] Added q_objs to __call__ args --- mongoengine/queryset.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 60f3f8df..9c9cc8da 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -149,9 +149,10 @@ class QuerySet(object): def __call__(self, *q_objs, **query): """Filter the selected documents by calling the - :class:`~mongoengine.QuerySet` with a query. + :class:`~mongoengine.queryset.QuerySet` with a query. - :param q_objs: :class:`~mongoengine.Q` objects to be used in the query + :param q_objs: :class:`~mongoengine.queryset.Q` objects to be used in + the query :param query: Django-style query keyword arguments """ for q in q_objs: @@ -161,7 +162,9 @@ class QuerySet(object): return self def filter(self, *q_objs, **query): - return self.__call__(**query) + """An alias of :meth:`~mongoengine.queryset.QuerySet.__call__` + """ + return self.__call__(*q_objs, **query) @property def _collection(self): @@ -327,7 +330,16 @@ class QuerySet(object): """ # Slice provided if isinstance(key, slice): - self._cursor_obj = self._cursor[key] + try: + self._cursor_obj = self._cursor[key] + except IndexError, err: + # PyMongo raises an error if key.start == key.stop, catch it, + # bin it, kill it. + if key.start >=0 and key.stop >= 0 and key.step is None: + if key.start == key.stop: + self.limit(0) + return self + raise err # Allow further QuerySet modifications to be performed return self # Integer index provided From ce69428cc636801d550e9d59509043480da662b2 Mon Sep 17 00:00:00 2001 From: Harry Marr Date: Wed, 13 Jan 2010 16:41:57 +0000 Subject: [PATCH 2/5] Moved validate() to BaseDocument --- docs/django.rst | 4 ++++ mongoengine/base.py | 19 +++++++++++++++++++ mongoengine/document.py | 19 ------------------- tests/document.py | 19 +++++++++++++++++++ 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/docs/django.rst b/docs/django.rst index d2c468a2..92a8a52b 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -28,6 +28,8 @@ The :mod:`~mongoengine.django.auth` module also contains a :func:`~mongoengine.django.auth.get_user` helper function, that takes a user's :attr:`id` and returns a :class:`~mongoengine.django.auth.User` object. +.. versionadded:: 0.1.3 + Sessions ======== Django allows the use of different backend stores for its sessions. MongoEngine @@ -40,3 +42,5 @@ session backend, ensure that your settings module has into you settings module:: SESSION_ENGINE = 'mongoengine.django.sessions' + +.. versionadded:: 0.2.1 diff --git a/mongoengine/base.py b/mongoengine/base.py index 9ac35607..cf97f875 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -246,6 +246,25 @@ class BaseDocument(object): value = getattr(self, attr_name, None) setattr(self, attr_name, value) + def validate(self): + """Ensure that all fields' values are valid and that required fields + are present. + """ + # Get a list of tuples of field names and their current values + fields = [(field, getattr(self, name)) + for name, field in self._fields.items()] + + # Ensure that each field is matched to a valid value + for field, value in fields: + if value is not None: + try: + field.validate(value) + except (ValueError, AttributeError, AssertionError), e: + raise ValidationError('Invalid value for field of type "' + + field.__class__.__name__ + '"') + elif field.required: + raise ValidationError('Field "%s" is required' % field.name) + @classmethod def _get_subclasses(cls): """Return a dictionary of all subclasses (found recursively). diff --git a/mongoengine/document.py b/mongoengine/document.py index 3492fd76..62f9ecce 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -107,25 +107,6 @@ class Document(BaseDocument): for field in self._fields: setattr(self, field, obj[field]) - def validate(self): - """Ensure that all fields' values are valid and that required fields - are present. - """ - # Get a list of tuples of field names and their current values - fields = [(field, getattr(self, name)) - for name, field in self._fields.items()] - - # Ensure that each field is matched to a valid value - for field, value in fields: - if value is not None: - try: - field.validate(value) - except (ValueError, AttributeError, AssertionError), e: - raise ValidationError('Invalid value for field of type "' + - field.__class__.__name__ + '"') - elif field.required: - raise ValidationError('Field "%s" is required' % field.name) - @classmethod def drop_collection(cls): """Drops the entire collection associated with this diff --git a/tests/document.py b/tests/document.py index 91b09e75..2aa98811 100644 --- a/tests/document.py +++ b/tests/document.py @@ -387,6 +387,25 @@ class DocumentTest(unittest.TestCase): self.assertTrue('content' in Comment._fields) self.assertFalse('id' in Comment._fields) self.assertFalse(hasattr(Comment, '_meta')) + + def test_embedded_document_validation(self): + """Ensure that embedded documents may be validated. + """ + class Comment(EmbeddedDocument): + date = DateTimeField() + content = StringField(required=True) + + comment = Comment() + self.assertRaises(ValidationError, comment.validate) + + comment.content = 'test' + comment.validate() + + comment.date = 4 + self.assertRaises(ValidationError, comment.validate) + + comment.date = datetime.datetime.now() + comment.validate() def test_save(self): """Ensure that a document may be saved in the database. From 1cdeb8130de6a6f094df09832a2b9416f11281bb Mon Sep 17 00:00:00 2001 From: blackbrrr Date: Thu, 14 Jan 2010 11:32:01 -0600 Subject: [PATCH 3/5] ObjectIdField.to_python returns pymongo.objectid.ObjectId --- mongoengine/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/base.py b/mongoengine/base.py index 9ac35607..fb42389b 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -69,7 +69,8 @@ class ObjectIdField(BaseField): """ def to_python(self, value): - return unicode(value) + return value + # return unicode(value) def to_mongo(self, value): if not isinstance(value, pymongo.objectid.ObjectId): From 5f84d6f8f85f2400db54b686f357f1fb658e2d1e Mon Sep 17 00:00:00 2001 From: blackbrrr Date: Thu, 14 Jan 2010 11:32:28 -0600 Subject: [PATCH 4/5] added URLField, DecimalField, tests. --- mongoengine/fields.py | 92 ++++++++++++++++++++++++++++++++++++------- tests/document.py | 2 +- tests/fields.py | 36 +++++++++++++++++ 3 files changed, 114 insertions(+), 16 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 664edb53..75bfd317 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1,26 +1,37 @@ from base import BaseField, ObjectIdField, ValidationError from document import Document, EmbeddedDocument from connection import _get_db - + import re import pymongo import datetime +import decimal __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField', - 'DateTimeField', 'EmbeddedDocumentField', 'ListField', - 'ObjectIdField', 'ReferenceField', 'ValidationError'] + 'DateTimeField', 'EmbeddedDocumentField', 'ListField', + 'ObjectIdField', 'ReferenceField', 'ValidationError', + 'URLField', 'DecimalField'] + + +URL_REGEX = re.compile( + r'^https?://' + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' + r'localhost|' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$', re.IGNORECASE) class StringField(BaseField): """A unicode string field. """ - + def __init__(self, regex=None, max_length=None, **kwargs): self.regex = re.compile(regex) if regex else None self.max_length = max_length super(StringField, self).__init__(**kwargs) - + def to_python(self, value): return unicode(value) @@ -38,6 +49,25 @@ class StringField(BaseField): return None +class URLField(BaseField): + """A field that validates input as a URL. + """ + + def __init__(self, verify_exists=True, **kwargs): + self.verify_exists = verify_exists + super(URLField, self).__init__(**kwargs) + + def validate(self, value): + import urllib2 + + if self.verify_exists: + try: + request = urllib2.Request(value) + response = urllib2.urlopen(request) + except Exception, e: + raise ValidationError('This URL appears to be invalid: %s' % e) + + class IntField(BaseField): """An integer field. """ @@ -45,12 +75,15 @@ class IntField(BaseField): def __init__(self, min_value=None, max_value=None, **kwargs): self.min_value, self.max_value = min_value, max_value super(IntField, self).__init__(**kwargs) - + def to_python(self, value): return int(value) def validate(self, value): - assert isinstance(value, (int, long)) + try: + value = int(value) + except: + raise ValidationError('%s could not be converted to int' % value) if self.min_value is not None and value < self.min_value: raise ValidationError('Integer value is too small') @@ -66,7 +99,7 @@ class FloatField(BaseField): def __init__(self, min_value=None, max_value=None, **kwargs): self.min_value, self.max_value = min_value, max_value super(FloatField, self).__init__(**kwargs) - + def to_python(self, value): return float(value) @@ -80,12 +113,41 @@ class FloatField(BaseField): raise ValidationError('Float value is too large') +class DecimalField(BaseField): + """A fixed-point decimal number field. + """ + + def __init__(self, min_value=None, max_value=None, **kwargs): + self.min_value, self.max_value = min_value, max_value + super(DecimalField, self).__init__(**kwargs) + + def to_python(self, value): + if not isinstance(value, basestring): + value = unicode(value) + return decimal.Decimal(value) + + def validate(self, value): + if not isinstance(value, decimal.Decimal): + if not isinstance(value, basestring): + value = str(value) + try: + value = decimal.Decimal(value) + except Exception, exc: + raise ValidationError('Could not convert to decimal: %s' % exc) + + if self.min_value is not None and value < self.min_value: + raise ValidationError('Decimal value is too small') + + if self.max_value is not None and vale > self.max_value: + raise ValidationError('Decimal value is too large') + + class BooleanField(BaseField): """A boolean field type. .. versionadded:: 0.1.2 """ - + def to_python(self, value): return bool(value) @@ -102,8 +164,8 @@ class DateTimeField(BaseField): class EmbeddedDocumentField(BaseField): - """An embedded document field. Only valid values are subclasses of - :class:`~mongoengine.EmbeddedDocument`. + """An embedded document field. Only valid values are subclasses of + :class:`~mongoengine.EmbeddedDocument`. """ def __init__(self, document, **kwargs): @@ -112,7 +174,7 @@ class EmbeddedDocumentField(BaseField): 'to an EmbeddedDocumentField') self.document = document super(EmbeddedDocumentField, self).__init__(**kwargs) - + def to_python(self, value): if not isinstance(value, self.document): return self.document._from_son(value) @@ -122,7 +184,7 @@ class EmbeddedDocumentField(BaseField): return self.document.to_mongo(value) def validate(self, value): - """Make sure that the document instance is an instance of the + """Make sure that the document instance is an instance of the EmbeddedDocument subclass provided when the document was defined. """ # Using isinstance also works for subclasses of self.document @@ -202,7 +264,7 @@ class ReferenceField(BaseField): value = _get_db().dereference(value) if value is not None: instance._data[self.name] = self.document_type._from_son(value) - + return super(ReferenceField, self).__get__(instance, owner) def to_mongo(self, document): @@ -222,7 +284,7 @@ class ReferenceField(BaseField): collection = self.document_type._meta['collection'] return pymongo.dbref.DBRef(collection, id_) - + def prepare_query_value(self, value): return self.to_mongo(value) diff --git a/tests/document.py b/tests/document.py index 91b09e75..fea7f6b1 100644 --- a/tests/document.py +++ b/tests/document.py @@ -399,7 +399,7 @@ class DocumentTest(unittest.TestCase): person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(person_obj['name'], 'Test User') self.assertEqual(person_obj['age'], 30) - self.assertEqual(str(person_obj['_id']), person.id) + self.assertEqual(person_obj['_id'], person.id) def test_delete(self): """Ensure that document may be deleted using the delete method. diff --git a/tests/fields.py b/tests/fields.py index affb0c9b..3b39f0fc 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -1,5 +1,6 @@ import unittest import datetime +from decimal import Decimal from mongoengine import * from mongoengine.connection import _get_db @@ -78,6 +79,23 @@ class FieldTest(unittest.TestCase): person.name = 'Shorter name' person.validate() + + def test_url_validation(self): + """Ensure that invalid URLs cannot be assigned to URL fields. + """ + class Person(Document): + name = StringField() + personal_blog = URLField() + + person = Person() + person.name = "Guido van Rossum" + person.personal_blog = "pep8 or bust!" + + self.assertRaises(ValidationError, person.validate) + + # swap in a real URL + person.personal_blog = "http://neopythonic.blogspot.com/" + person.validate() def test_int_validation(self): """Ensure that invalid values cannot be assigned to int fields. @@ -112,6 +130,24 @@ class FieldTest(unittest.TestCase): self.assertRaises(ValidationError, person.validate) person.height = 4.0 self.assertRaises(ValidationError, person.validate) + + def test_decimal_validation(self): + """Ensure that invalid values cannot be assigned to decimal fields. + """ + class AlbumReview(Document): + score = DecimalField() + + review = AlbumReview() + review.score = "8.7" + review.validate() + review.score = Decimal("10.0") + review.validate() + # implicit conversion from float to string + review.score = 3.14 + review.validate() + + review.score = "it stinks!" + self.assertRaises(ValidationError, review.validate) def test_boolean_validation(self): """Ensure that invalid values cannot be assigned to boolean fields. From 972235cf0676523cd06f8bcfe1bdb3046ade48f8 Mon Sep 17 00:00:00 2001 From: blackbrrr Date: Thu, 14 Jan 2010 11:37:07 -0600 Subject: [PATCH 5/5] added build, dist, egg dirs to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 95ad521c..42dcc6e6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ .*.swp docs/.build docs/_build +build/ +dist/ +mongoengine.egg-info/ \ No newline at end of file