From 5f84d6f8f85f2400db54b686f357f1fb658e2d1e Mon Sep 17 00:00:00 2001 From: blackbrrr Date: Thu, 14 Jan 2010 11:32:28 -0600 Subject: [PATCH] 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.