diff options
| author | Alex Gaynor <alex.gaynor@gmail.com> | 2009-12-13 22:15:08 +0000 |
|---|---|---|
| committer | Alex Gaynor <alex.gaynor@gmail.com> | 2009-12-13 22:15:08 +0000 |
| commit | 2794cceb5f6cf21f2c99b90c9d1e79475f830460 (patch) | |
| tree | 708273f4e4cd96ca64c6f18e8de17d1055cd761f | |
| parent | 049dc42bdea8f524f9d52edd4ddc05ef0048d401 (diff) | |
| download | django-2794cceb5f6cf21f2c99b90c9d1e79475f830460.tar.gz | |
[soc2009/multidb] Merged up to trunk r11858.
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11860 bcc190cf-cafb-0310-a4f2-bffc1f526a37
53 files changed, 1660 insertions, 1623 deletions
@@ -55,6 +55,7 @@ answer newbie questions, and generally made Django that much better: Niran Babalola <niran@niran.org> Morten Bagai <m@bagai.com> Mikaël Barbero <mikael.barbero nospam at nospam free.fr> + Randy Barlow <randy@electronsweatshop.com> Scott Barr <scott@divisionbyzero.com.au> Jiri Barton Ned Batchelder <http://www.nedbatchelder.com/> @@ -136,7 +137,7 @@ answer newbie questions, and generally made Django that much better: Andrew Durdin <adurdin@gmail.com> dusk@woofle.net Andy Dustman <farcepest@gmail.com> - J. Clifford Dyer <jcd@unc.edu> + J. Clifford Dyer <jcd@sdf.lonestar.org> Clint Ecker Nick Efford <nick@efford.org> eibaan@gmail.com @@ -172,6 +173,7 @@ answer newbie questions, and generally made Django that much better: Alex Gaynor <alex.gaynor@gmail.com> Andy Gayton <andy-django@thecablelounge.com> Idan Gazit + geber@datacollect.com Baishampayan Ghose Dimitris Glezos <dimitris@glezos.com> glin@seznam.cz @@ -267,6 +269,7 @@ answer newbie questions, and generally made Django that much better: Finn Gruwier Larsen <finn@gruwier.dk> Lau Bech Lauritzen Rune Rønde Laursen <runerl@skjoldhoej.dk> + Mark Lavin <markdlavin@gmail.com> Eugene Lazutkin <http://lazutkin.com/blog/> lcordier@point45.com Jeong-Min Lee <falsetru@gmail.com> @@ -300,6 +303,7 @@ answer newbie questions, and generally made Django that much better: Jason McBrayer <http://www.carcosa.net/jason/> Kevin McConnell <kevin.mcconnell@gmail.com> mccutchen@gmail.com + Paul McLanahan <paul@mclanahan.net> Tobias McNulty <http://www.caktusgroup.com/blog> Christian Metts michael.mcewan@gmail.com @@ -382,6 +386,7 @@ answer newbie questions, and generally made Django that much better: Massimo Scamarcia <massimo.scamarcia@gmail.com> David Schein Bernd Schlapsi + schwank@gmail.com scott@staplefish.com Ilya Semenov <semenov@inetss.com> serbaut@gmail.com @@ -393,6 +398,7 @@ answer newbie questions, and generally made Django that much better: Jozko Skrablin <jozko.skrablin@gmail.com> Ben Slavin <benjamin.slavin@gmail.com> sloonz <simon.lipp@insa-lyon.fr> + Paul Smith <blinkylights23@gmail.com> Warren Smith <warren@wandrsmith.net> smurf@smurf.noris.de Vsevolod Solovyov @@ -27,7 +27,7 @@ http://code.djangoproject.com/newticket To get more help: * Join the #django channel on irc.freenode.net. Lots of helpful people - hang out there. Read the archives at http://oebfare.com/logger/django/. + hang out there. Read the archives at http://botland.oebfare.com/logger/django/. * Join the django-users mailing list, or read the archives, at http://groups.google.com/group/django-users. diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 7193beeee8..c055f4ea85 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -692,6 +692,9 @@ class ModelAdmin(BaseModelAdmin): # perform an action on it, so bail. selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) if not selected: + # Reminder that something needs to be selected or nothing will happen + msg = "Items must be selected in order to perform actions on them. No items have been changed." + self.message_user(request, _(msg)) return None response = func(self, request, queryset.filter(pk__in=selected)) @@ -703,6 +706,9 @@ class ModelAdmin(BaseModelAdmin): return response else: return HttpResponseRedirect(".") + else: + msg = "No action selected." + self.message_user(request, _(msg)) @csrf_protect @transaction.commit_on_success diff --git a/django/contrib/admindocs/models.py b/django/contrib/admindocs/models.py new file mode 100644 index 0000000000..a9f813a4cb --- /dev/null +++ b/django/contrib/admindocs/models.py @@ -0,0 +1 @@ +# Empty models.py to allow for specifying admindocs as a test label. diff --git a/django/contrib/admindocs/tests/__init__.py b/django/contrib/admindocs/tests/__init__.py new file mode 100644 index 0000000000..a091ebe122 --- /dev/null +++ b/django/contrib/admindocs/tests/__init__.py @@ -0,0 +1,36 @@ +import unittest +from django.contrib.admindocs import views +import fields + +from django.db.models import fields as builtin_fields + +class TestFieldType(unittest.TestCase): + def setUp(self): + pass + + def test_field_name(self): + self.assertRaises(AttributeError, + views.get_readable_field_data_type, "NotAField" + ) + + def test_builtin_fields(self): + self.assertEqual( + views.get_readable_field_data_type(builtin_fields.BooleanField()), + u'Boolean (Either True or False)' + ) + + def test_custom_fields(self): + self.assertEqual( + views.get_readable_field_data_type(fields.CustomField()), + u'A custom field type' + ) + self.assertEqual( + views.get_readable_field_data_type(fields.DocstringLackingField()), + u'Field of type: DocstringLackingField' + ) + + def test_multiline_custom_field_truncation(self): + self.assertEqual( + views.get_readable_field_data_type(fields.ManyLineDocstringField()), + u'Many-line custom field' + ) diff --git a/django/contrib/admindocs/tests/fields.py b/django/contrib/admindocs/tests/fields.py new file mode 100644 index 0000000000..5cab3627c6 --- /dev/null +++ b/django/contrib/admindocs/tests/fields.py @@ -0,0 +1,13 @@ +from django.db import models + +class CustomField(models.Field): + """A custom field type""" + +class ManyLineDocstringField(models.Field): + """Many-line custom field + + This docstring has many lines. Lorum ipsem etc. etc. Four score + and seven years ago, and so on and so forth.""" + +class DocstringLackingField(models.Field): + pass diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 571f393ff8..d04030e9f0 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -326,43 +326,20 @@ def get_return_data_type(func_name): return 'Integer' return '' -# Maps Field objects to their human-readable data types, as strings. -# Column-type strings can contain format strings; they'll be interpolated -# against the values of Field.__dict__ before being output. -# If a column type is set to None, it won't be included in the output. -DATA_TYPE_MAPPING = { - 'AutoField' : _('Integer'), - 'BooleanField' : _('Boolean (Either True or False)'), - 'CharField' : _('String (up to %(max_length)s)'), - 'CommaSeparatedIntegerField': _('Comma-separated integers'), - 'DateField' : _('Date (without time)'), - 'DateTimeField' : _('Date (with time)'), - 'DecimalField' : _('Decimal number'), - 'EmailField' : _('E-mail address'), - 'FileField' : _('File path'), - 'FilePathField' : _('File path'), - 'FloatField' : _('Floating point number'), - 'ForeignKey' : _('Integer'), - 'ImageField' : _('File path'), - 'IntegerField' : _('Integer'), - 'IPAddressField' : _('IP address'), - 'ManyToManyField' : '', - 'NullBooleanField' : _('Boolean (Either True, False or None)'), - 'OneToOneField' : _('Relation to parent model'), - 'PhoneNumberField' : _('Phone number'), - 'PositiveIntegerField' : _('Integer'), - 'PositiveSmallIntegerField' : _('Integer'), - 'SlugField' : _('String (up to %(max_length)s)'), - 'SmallIntegerField' : _('Integer'), - 'TextField' : _('Text'), - 'TimeField' : _('Time'), - 'URLField' : _('URL'), - 'USStateField' : _('U.S. state (two uppercase letters)'), - 'XMLField' : _('XML text'), -} - def get_readable_field_data_type(field): - return DATA_TYPE_MAPPING[field.get_internal_type()] % field.__dict__ + """Returns the first line of a doc string for a given field type, if it + exists. Fields' docstrings can contain format strings, which will be + interpolated against the values of Field.__dict__ before being output. + If no docstring is given, a sensible value will be auto-generated from + the field's class name.""" + + if field.__doc__: + doc = field.__doc__.split('\n')[0] + return _(doc) % field.__dict__ + else: + return _(u'Field of type: %(field_type)s') % { + 'field_type': field.__class__.__name__ + } def extract_views_from_urlpatterns(urlpatterns, base=''): """ diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 7c5fb14ee9..729b0cf354 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -40,8 +40,8 @@ def get_srid_info(srid, connection): return _srid_cache[name][srid] -class GeometryField(Field): - "The base GIS field -- maps to the OpenGIS Specification Geometry type." +class GeometryField(SpatialBackend.Field): + """The base GIS field -- maps to the OpenGIS Specification Geometry type.""" # The OpenGIS Geometry name. geom_type = 'GEOMETRY' @@ -285,22 +285,29 @@ class GeometryField(Field): # The OpenGIS Geometry Type Fields class PointField(GeometryField): + """Point""" geom_type = 'POINT' class LineStringField(GeometryField): + """Line string""" geom_type = 'LINESTRING' class PolygonField(GeometryField): + """Polygon""" geom_type = 'POLYGON' class MultiPointField(GeometryField): + """Multi-point""" geom_type = 'MULTIPOINT' class MultiLineStringField(GeometryField): + """Multi-line string""" geom_type = 'MULTILINESTRING' class MultiPolygonField(GeometryField): + """Multi polygon""" geom_type = 'MULTIPOLYGON' class GeometryCollectionField(GeometryField): + """Geometry collection""" geom_type = 'GEOMETRYCOLLECTION' diff --git a/django/contrib/localflavor/ca/forms.py b/django/contrib/localflavor/ca/forms.py index 327d938373..542ff40670 100644 --- a/django/contrib/localflavor/ca/forms.py +++ b/django/contrib/localflavor/ca/forms.py @@ -12,13 +12,20 @@ phone_digits_re = re.compile(r'^(?:1-?)?(\d{3})[-\.]?(\d{3})[-\.]?(\d{4})$') sin_re = re.compile(r"^(\d{3})-(\d{3})-(\d{3})$") class CAPostalCodeField(RegexField): - """Canadian postal code field.""" + """ + Canadian postal code field. + + Validates against known invalid characters: D, F, I, O, Q, U + Additionally the first character cannot be Z or W. + For more info see: + http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp#1402170 + """ default_error_messages = { 'invalid': _(u'Enter a postal code in the format XXX XXX.'), } def __init__(self, *args, **kwargs): - super(CAPostalCodeField, self).__init__(r'^[ABCEGHJKLMNPRSTVXYZ]\d[A-Z] \d[A-Z]\d$', + super(CAPostalCodeField, self).__init__(r'^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] \d[ABCEGHJKLMNPRSTVWXYZ]\d$', max_length=None, min_length=None, *args, **kwargs) class CAPhoneNumberField(Field): diff --git a/django/contrib/localflavor/us/models.py b/django/contrib/localflavor/us/models.py index 209bb9051e..01157d038b 100644 --- a/django/contrib/localflavor/us/models.py +++ b/django/contrib/localflavor/us/models.py @@ -1,22 +1,16 @@ -from django.db.models.fields import Field - -class USStateField(Field): - def get_internal_type(self): - return "USStateField" - - def db_type(self, connection): - if connection.settings_dict['ENGINE'] == 'django.db.backends.oracle': - return 'CHAR(2)' - else: - return 'varchar(2)' - - def formfield(self, **kwargs): - from django.contrib.localflavor.us.forms import USStateSelect - defaults = {'widget': USStateSelect} - defaults.update(kwargs) - return super(USStateField, self).formfield(**defaults) - +from django.conf import settings +from django.db.models.fields import Field, CharField +from django.contrib.localflavor.us.us_states import STATE_CHOICES + +class USStateField(CharField): + """U.S. state (two uppercase letters)""" + def __init__(self, *args, **kwargs): + kwargs['choices'] = STATE_CHOICES + kwargs['max_length'] = 2 + super(USStateField, self).__init__(*args, **kwargs) + class PhoneNumberField(Field): + """Phone number""" def get_internal_type(self): return "PhoneNumberField" diff --git a/django/contrib/messages/storage/cookie.py b/django/contrib/messages/storage/cookie.py index 248791d3e7..1ac1afe550 100644 --- a/django/contrib/messages/storage/cookie.py +++ b/django/contrib/messages/storage/cookie.py @@ -1,7 +1,7 @@ import hmac from django.conf import settings -from django.utils.hashcompat import sha_constructor +from django.utils.hashcompat import sha_hmac from django.contrib.messages import constants from django.contrib.messages.storage.base import BaseStorage, Message from django.utils import simplejson as json @@ -41,7 +41,6 @@ class MessageDecoder(json.JSONDecoder): decoded = super(MessageDecoder, self).decode(s, **kwargs) return self.process_messages(decoded) - class CookieStorage(BaseStorage): """ Stores messages in a cookie. @@ -103,7 +102,7 @@ class CookieStorage(BaseStorage): SECRET_KEY, modified to make it unique for the present purpose. """ key = 'django.contrib.messages' + settings.SECRET_KEY - return hmac.new(key, value, sha_constructor).hexdigest() + return hmac.new(key, value, sha_hmac).hexdigest() def _encode(self, messages, encode_empty=False): """ diff --git a/django/contrib/messages/tests/base.py b/django/contrib/messages/tests/base.py index 45e061aedc..6ca9185f62 100644 --- a/django/contrib/messages/tests/base.py +++ b/django/contrib/messages/tests/base.py @@ -218,9 +218,9 @@ class BaseTest(TestCase): response = self.client.post(add_url, data, follow=True) self.assertRedirects(response, show_url) self.assertTrue('messages' in response.context) - self.assertEqual(list(response.context['messages']), - data['messages']) + context_messages = list(response.context['messages']) for msg in data['messages']: + self.assertTrue(msg in context_messages) self.assertContains(response, msg) def test_middleware_disabled_anon_user(self): diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index 0ff0da32ae..d5e6394f92 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -46,7 +46,28 @@ class CacheClass(BaseCache): self._cache.disconnect_all() def incr(self, key, delta=1): - return self._cache.incr(key, delta) + try: + val = self._cache.incr(key, delta) + + # python-memcache responds to incr on non-existent keys by + # raising a ValueError. Cmemcache returns None. In both + # cases, we should raise a ValueError though. + except ValueError: + val = None + if val is None: + raise ValueError("Key '%s' not found" % key) + + return val def decr(self, key, delta=1): - return self._cache.decr(key, delta) + try: + val = self._cache.decr(key, delta) + + # python-memcache responds to decr on non-existent keys by + # raising a ValueError. Cmemcache returns None. In both + # cases, we should raise a ValueError though. + except ValueError: + val = None + if val is None: + raise ValueError("Key '%s' not found" % key) + return val diff --git a/django/core/management/commands/test.py b/django/core/management/commands/test.py index 8ebf3daea6..4fd6ba0c8d 100644 --- a/django/core/management/commands/test.py +++ b/django/core/management/commands/test.py @@ -6,6 +6,8 @@ class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--noinput', action='store_false', dest='interactive', default=True, help='Tells Django to NOT prompt the user for input of any kind.'), + make_option('--failfast', action='store_true', dest='failfast', default=False, + help='Tells Django to stop running the test suite after first failed test.') ) help = 'Runs the test suite for the specified applications, or the entire site if no apps are specified.' args = '[appname ...]' @@ -15,11 +17,18 @@ class Command(BaseCommand): def handle(self, *test_labels, **options): from django.conf import settings from django.test.utils import get_runner - + verbosity = int(options.get('verbosity', 1)) interactive = options.get('interactive', True) + failfast = options.get('failfast', False) test_runner = get_runner(settings) - failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive) + # Some custom test runners won't accept the failfast flag, so let's make sure they accept it before passing it to them + if 'failfast' in test_runner.func_code.co_varnames: + failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive, + failfast=failfast) + else: + failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive) + if failures: sys.exit(failures) diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index a924afeaf8..c721015d62 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -257,9 +257,8 @@ class RegexURLResolver(object): def _resolve_special(self, view_type): callback = getattr(self.urlconf_module, 'handler%s' % view_type) - mod_name, func_name = get_mod_func(callback) try: - return getattr(import_module(mod_name), func_name), {} + return get_callable(callback), {} except (ImportError, AttributeError), e: raise ViewDoesNotExist, "Tried %s. Error was: %s" % (callback, str(e)) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 97af08612f..8012ade37c 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -3,10 +3,6 @@ import datetime import os import re import time -try: - import decimal -except ImportError: - from django.utils import _decimal as decimal # for Python 2.3 from django.db import connection from django.db.models import signals @@ -50,7 +46,9 @@ class FieldDoesNotExist(Exception): # getattr(obj, opts.pk.attname) class Field(object): + """Base class for all field types""" __metaclass__ = LegacyConnection + # Designates whether empty strings fundamentally are allowed at the # database level. empty_strings_allowed = True @@ -370,7 +368,10 @@ class Field(object): return getattr(obj, self.attname) class AutoField(Field): + """Integer""" + empty_strings_allowed = False + def __init__(self, *args, **kwargs): assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__ kwargs['blank'] = True @@ -400,7 +401,10 @@ class AutoField(Field): return None class BooleanField(Field): + """Boolean (Either True or False)""" + empty_strings_allowed = False + def __init__(self, *args, **kwargs): kwargs['blank'] = True if 'default' not in kwargs and not kwargs.get('null'): @@ -443,6 +447,8 @@ class BooleanField(Field): return super(BooleanField, self).formfield(**defaults) class CharField(Field): + """String (up to %(max_length)s)""" + def get_internal_type(self): return "CharField" @@ -464,6 +470,8 @@ class CharField(Field): # TODO: Maybe move this into contrib, because it's specialized. class CommaSeparatedIntegerField(CharField): + """Comma-separated integers""" + def formfield(self, **kwargs): defaults = { 'form_class': forms.RegexField, @@ -479,7 +487,10 @@ class CommaSeparatedIntegerField(CharField): ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$') class DateField(Field): + """Date (without time)""" + empty_strings_allowed = False + def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add #HACKs : auto_now_add/auto_now should be done as a default or a pre_save. @@ -559,6 +570,8 @@ class DateField(Field): return super(DateField, self).formfield(**defaults) class DateTimeField(DateField): + """Date (with time)""" + def get_internal_type(self): return "DateTimeField" @@ -623,7 +636,10 @@ class DateTimeField(DateField): return super(DateTimeField, self).formfield(**defaults) class DecimalField(Field): + """Decimal number""" + empty_strings_allowed = False + def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs): self.max_digits, self.decimal_places = max_digits, decimal_places Field.__init__(self, verbose_name, name, **kwargs) @@ -677,6 +693,8 @@ class DecimalField(Field): return super(DecimalField, self).formfield(**defaults) class EmailField(CharField): + """E-mail address""" + def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) @@ -687,6 +705,8 @@ class EmailField(CharField): return super(EmailField, self).formfield(**defaults) class FilePathField(Field): + """File path""" + def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs): self.path, self.match, self.recursive = path, match, recursive kwargs['max_length'] = kwargs.get('max_length', 100) @@ -706,6 +726,8 @@ class FilePathField(Field): return "FilePathField" class FloatField(Field): + """Floating point number""" + empty_strings_allowed = False def get_prep_value(self, value): @@ -731,8 +753,15 @@ class FloatField(Field): return super(FloatField, self).formfield(**defaults) class IntegerField(Field): + """Integer""" + empty_strings_allowed = False +<<<<<<< HEAD:django/db/models/fields/__init__.py def get_prep_value(self, value): +======= + + def get_db_prep_value(self, value): +>>>>>>> master:django/db/models/fields/__init__.py if value is None: return None return int(value) @@ -755,7 +784,10 @@ class IntegerField(Field): return super(IntegerField, self).formfield(**defaults) class IPAddressField(Field): + """IP address""" + empty_strings_allowed = False + def __init__(self, *args, **kwargs): kwargs['max_length'] = 15 Field.__init__(self, *args, **kwargs) @@ -769,7 +801,10 @@ class IPAddressField(Field): return super(IPAddressField, self).formfield(**defaults) class NullBooleanField(Field): + """Boolean (Either True, False or None)""" + empty_strings_allowed = False + def __init__(self, *args, **kwargs): kwargs['null'] = True Field.__init__(self, *args, **kwargs) @@ -809,6 +844,8 @@ class NullBooleanField(Field): return super(NullBooleanField, self).formfield(**defaults) class PositiveIntegerField(IntegerField): + """Integer""" + def get_internal_type(self): return "PositiveIntegerField" @@ -818,6 +855,8 @@ class PositiveIntegerField(IntegerField): return super(PositiveIntegerField, self).formfield(**defaults) class PositiveSmallIntegerField(IntegerField): + """Integer""" + def get_internal_type(self): return "PositiveSmallIntegerField" @@ -827,6 +866,8 @@ class PositiveSmallIntegerField(IntegerField): return super(PositiveSmallIntegerField, self).formfield(**defaults) class SlugField(CharField): + """String (up to %(max_length)s)""" + def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 50) # Set db_index=True unless it's been set manually. @@ -843,10 +884,14 @@ class SlugField(CharField): return super(SlugField, self).formfield(**defaults) class SmallIntegerField(IntegerField): + """Integer""" + def get_internal_type(self): return "SmallIntegerField" class TextField(Field): + """Text""" + def get_internal_type(self): return "TextField" @@ -856,7 +901,10 @@ class TextField(Field): return super(TextField, self).formfield(**defaults) class TimeField(Field): + """Time""" + empty_strings_allowed = False + def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add if auto_now or auto_now_add: @@ -933,6 +981,8 @@ class TimeField(Field): return super(TimeField, self).formfield(**defaults) class URLField(CharField): + """URL""" + def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 200) self.verify_exists = verify_exists @@ -944,6 +994,8 @@ class URLField(CharField): return super(URLField, self).formfield(**defaults) class XMLField(TextField): + """XML text""" + def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs): self.schema_path = schema_path Field.__init__(self, verbose_name, name, **kwargs) diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 2eb22271cb..3debb25cb6 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -209,6 +209,8 @@ class FileDescriptor(object): instance.__dict__[self.field.name] = value class FileField(Field): + """File path""" + # The class to wrap instance attributes in. Accessing the file object off # the instance will always return an instance of attr_class. attr_class = FieldFile @@ -323,6 +325,8 @@ class ImageFieldFile(ImageFile, FieldFile): super(ImageFieldFile, self).delete(save) class ImageField(FileField): + """File path""" + attr_class = ImageFieldFile descriptor_class = ImageFileDescriptor diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index f8ae5b1f4e..84a0a7a74f 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -708,6 +708,8 @@ class ManyToManyRel(object): return self.to._meta.pk class ForeignKey(RelatedField, Field): + """Foreign Key (type determined by related field)""" + empty_strings_allowed = False def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: @@ -806,12 +808,13 @@ class ForeignKey(RelatedField, Field): return rel_field.db_type(connection=connection) class OneToOneField(ForeignKey): - """ + """One-to-one relationship + A OneToOneField is essentially the same as a ForeignKey, with the exception that always carries a "unique" constraint with it and the reverse relation always returns the object pointed to (since there will only ever be one), - rather than returning a list. - """ + rather than returning a list.""" + def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -865,6 +868,8 @@ def create_many_to_many_intermediary_model(field, klass): }) class ManyToManyField(RelatedField, Field): + """Many-to-many relationship""" + def __init__(self, to, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) diff --git a/django/forms/forms.py b/django/forms/forms.py index 0b7c2e2338..e854de8a7a 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -138,6 +138,8 @@ class BaseForm(StrAndUnicode): "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." top_errors = self.non_field_errors() # Errors that should be displayed above all fields. output, hidden_fields = [], [] + html_class_attr = '' + for name, field in self.fields.items(): bf = BoundField(self, field, name) bf_errors = self.error_class([conditional_escape(error) for error in bf.errors]) # Escape and cache in local variable. @@ -146,8 +148,15 @@ class BaseForm(StrAndUnicode): top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors]) hidden_fields.append(unicode(bf)) else: + # Create a 'class="..."' atribute if the row should have any + # CSS classes applied. + css_classes = bf.css_classes() + if css_classes: + html_class_attr = ' class="%s"' % css_classes + if errors_on_separate_row and bf_errors: output.append(error_row % force_unicode(bf_errors)) + if bf.label: label = conditional_escape(force_unicode(bf.label)) # Only add the suffix if the label does not end in @@ -158,13 +167,23 @@ class BaseForm(StrAndUnicode): label = bf.label_tag(label) or '' else: label = '' + if field.help_text: help_text = help_text_html % force_unicode(field.help_text) else: help_text = u'' - output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text}) + + output.append(normal_row % { + 'errors': force_unicode(bf_errors), + 'label': force_unicode(label), + 'field': unicode(bf), + 'help_text': help_text, + 'html_class_attr': html_class_attr + }) + if top_errors: output.insert(0, error_row % force_unicode(top_errors)) + if hidden_fields: # Insert any hidden fields in the last row. str_hidden = u''.join(hidden_fields) if output: @@ -176,7 +195,9 @@ class BaseForm(StrAndUnicode): # that users write): if there are only top errors, we may # not be able to conscript the last row for our purposes, # so insert a new, empty row. - last_row = normal_row % {'errors': '', 'label': '', 'field': '', 'help_text': ''} + last_row = (normal_row % {'errors': '', 'label': '', + 'field': '', 'help_text':'', + 'html_class_attr': html_class_attr}) output.append(last_row) output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender else: @@ -187,15 +208,30 @@ class BaseForm(StrAndUnicode): def as_table(self): "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." - return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', u'<br />%s', False) + return self._html_output( + normal_row = u'<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>', + error_row = u'<tr><td colspan="2">%s</td></tr>', + row_ender = u'</td></tr>', + help_text_html = u'<br />%s', + errors_on_separate_row = False) def as_ul(self): "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." - return self._html_output(u'<li>%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False) + return self._html_output( + normal_row = u'<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>', + error_row = u'<li>%s</li>', + row_ender = '</li>', + help_text_html = u' %s', + errors_on_separate_row = False) def as_p(self): "Returns this form rendered as HTML <p>s." - return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'%s', '</p>', u' %s', True) + return self._html_output( + normal_row = u'<p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p>', + error_row = u'%s', + row_ender = '</p>', + help_text_html = u' %s', + errors_on_separate_row = True) def non_field_errors(self): """ @@ -343,6 +379,7 @@ class BoundField(StrAndUnicode): self.name = name self.html_name = form.add_prefix(name) self.html_initial_name = form.add_initial_prefix(name) + self.html_initial_id = form.add_initial_prefix(self.auto_id) if self.field.label is None: self.label = pretty_name(name) else: @@ -374,7 +411,10 @@ class BoundField(StrAndUnicode): attrs = attrs or {} auto_id = self.auto_id if auto_id and 'id' not in attrs and 'id' not in widget.attrs: - attrs['id'] = auto_id + if not only_initial: + attrs['id'] = auto_id + else: + attrs['id'] = self.html_initial_id if not self.form.is_bound: data = self.form.initial.get(self.name, self.field.initial) if callable(data): @@ -429,6 +469,19 @@ class BoundField(StrAndUnicode): contents = u'<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, unicode(contents)) return mark_safe(contents) + def css_classes(self, extra_classes=None): + """ + Returns a string of space-separated CSS classes for this field. + """ + if hasattr(extra_classes, 'split'): + extra_classes = extra_classes.split() + extra_classes = set(extra_classes or []) + if self.errors and hasattr(self.form, 'error_css_class'): + extra_classes.add(self.form.error_css_class) + if self.field.required and hasattr(self.form, 'required_css_class'): + extra_classes.add(self.form.required_css_class) + return ' '.join(extra_classes) + def _is_hidden(self): "Returns True if this BoundField's widget is hidden." return self.field.widget.is_hidden diff --git a/django/forms/models.py b/django/forms/models.py index 6985448659..81b727111e 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -912,6 +912,9 @@ class ModelChoiceIterator(object): for obj in self.queryset.all(): yield self.choice(obj) + def __len__(self): + return len(self.queryset) + def choice(self, obj): if self.field.to_field_name: key = obj.serializable_value(self.field.to_field_name) diff --git a/django/http/__init__.py b/django/http/__init__.py index 683212fcd4..446659b560 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -1,6 +1,6 @@ import os import re -from Cookie import SimpleCookie, CookieError +from Cookie import BaseCookie, SimpleCookie, CookieError from pprint import pformat from urllib import urlencode from urlparse import urljoin @@ -251,13 +251,15 @@ class QueryDict(MultiValueDict): def parse_cookie(cookie): if cookie == '': return {} - try: - c = SimpleCookie() - c.load(cookie) - except CookieError: - # Invalid cookie - return {} - + if not isinstance(cookie, BaseCookie): + try: + c = SimpleCookie() + c.load(cookie) + except CookieError: + # Invalid cookie + return {} + else: + c = cookie cookiedict = {} for key in c.keys(): cookiedict[key] = c.get(key).value diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 2957c3d045..a8c25670f6 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -249,7 +249,8 @@ stringformat.is_safe = True def title(value): """Converts a string into titlecase.""" - return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) + t = re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) + return re.sub("\d([A-Z])", lambda m: m.group(0).lower(), t) title.is_safe = True title = stringfilter(title) diff --git a/django/test/simple.py b/django/test/simple.py index 4c4d7324f9..092ef4bde2 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -10,6 +10,26 @@ TEST_MODULE = 'tests' doctestOutputChecker = OutputChecker() +class DjangoTestRunner(unittest.TextTestRunner): + + def __init__(self, verbosity=0, failfast=False, **kwargs): + super(DjangoTestRunner, self).__init__(verbosity=verbosity, **kwargs) + self.failfast = failfast + + def _makeResult(self): + result = super(DjangoTestRunner, self)._makeResult() + failfast = self.failfast + + def stoptest_override(func): + def stoptest(test): + if failfast and not result.wasSuccessful(): + result.stop() + func(test) + return stoptest + + setattr(result, 'stopTest', stoptest_override(result.stopTest)) + return result + def get_tests(app_module): try: app_path = app_module.__name__.split('.')[:-1] @@ -146,7 +166,7 @@ def reorder_suite(suite, classes): bins[0].addTests(bins[i+1]) return bins[0] -def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]): +def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=[]): """ Run the unit tests for all the test labels in the provided list. Labels must be of the form: @@ -186,13 +206,13 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]): suite = reorder_suite(suite, (TestCase,)) - old_names = [] from django.db import connections + old_names = [] for alias in connections: connection = connections[alias] old_names.append((connection, connection.settings_dict['NAME'])) connection.creation.create_test_db(verbosity, autoclobber=not interactive) - result = unittest.TextTestRunner(verbosity=verbosity).run(suite) + result = DjangoTestRunner(verbosity=verbosity, failfast=failfast).run(suite) for connection, old_name in old_names: connection.creation.destroy_test_db(old_name, verbosity) diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 85cdd443f8..2b586d7efe 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -1,3 +1,6 @@ +from copy import deepcopy + + class MergeDict(object): """ A simple class for creating new "virtual" dictionaries that actually look @@ -72,22 +75,20 @@ class SortedDict(dict): self.keyOrder.append(key) def __deepcopy__(self, memo): - from copy import deepcopy return self.__class__([(key, deepcopy(value, memo)) for key, value in self.iteritems()]) def __setitem__(self, key, value): - super(SortedDict, self).__setitem__(key, value) - if key not in self.keyOrder: + if key not in self: self.keyOrder.append(key) + super(SortedDict, self).__setitem__(key, value) def __delitem__(self, key): super(SortedDict, self).__delitem__(key) self.keyOrder.remove(key) def __iter__(self): - for k in self.keyOrder: - yield k + return iter(self.keyOrder) def pop(self, k, *args): result = super(SortedDict, self).pop(k, *args) @@ -108,7 +109,7 @@ class SortedDict(dict): def iteritems(self): for key in self.keyOrder: - yield key, super(SortedDict, self).__getitem__(key) + yield key, self[key] def keys(self): return self.keyOrder[:] @@ -117,18 +118,18 @@ class SortedDict(dict): return iter(self.keyOrder) def values(self): - return map(super(SortedDict, self).__getitem__, self.keyOrder) + return map(self.__getitem__, self.keyOrder) def itervalues(self): for key in self.keyOrder: - yield super(SortedDict, self).__getitem__(key) + yield self[key] def update(self, dict_): - for k, v in dict_.items(): - self.__setitem__(k, v) + for k, v in dict_.iteritems(): + self[k] = v def setdefault(self, key, default): - if key not in self.keyOrder: + if key not in self: self.keyOrder.append(key) return super(SortedDict, self).setdefault(key, default) @@ -222,18 +223,18 @@ class MultiValueDict(dict): dict.__setitem__(result, copy.deepcopy(key, memo), copy.deepcopy(value, memo)) return result - + def __getstate__(self): obj_dict = self.__dict__.copy() obj_dict['_data'] = dict([(k, self.getlist(k)) for k in self]) return obj_dict - + def __setstate__(self, obj_dict): data = obj_dict.pop('_data', {}) for k, v in data.items(): self.setlist(k, v) self.__dict__.update(obj_dict) - + def get(self, key, default=None): """ Returns the last data value for the passed key. If key doesn't exist @@ -301,12 +302,12 @@ class MultiValueDict(dict): def values(self): """Returns a list of the last value on every key list.""" return [self[key] for key in self.keys()] - + def itervalues(self): """Yield the last value on every key list.""" for key in self.iterkeys(): yield self[key] - + def copy(self): """Returns a copy of this object.""" return self.__deepcopy__() diff --git a/django/utils/functional.py b/django/utils/functional.py index 434b6b76c9..823cda4587 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -277,6 +277,13 @@ class LazyObject(object): self._setup() setattr(self._wrapped, name, value) + def __delattr__(self, name): + if name == "_wrapped": + raise TypeError("can't delete _wrapped.") + if self._wrapped is None: + self._setup() + delattr(self._wrapped, name) + def _setup(self): """ Must be implemented by subclasses to initialise the wrapped object. diff --git a/django/utils/hashcompat.py b/django/utils/hashcompat.py index 8880d92646..b1e6021890 100644 --- a/django/utils/hashcompat.py +++ b/django/utils/hashcompat.py @@ -8,9 +8,13 @@ available. try: import hashlib md5_constructor = hashlib.md5 + md5_hmac = md5_constructor sha_constructor = hashlib.sha1 + sha_hmac = sha_constructor except ImportError: import md5 md5_constructor = md5.new + md5_hmac = md5 import sha sha_constructor = sha.new + sha_hmac = sha diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index e1f04fe664..13e4fff36d 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -39,6 +39,8 @@ are traditionally called *north*, *east*, *south* and *west*. Our class looks something like this:: class Hand(object): + """A hand of cards (bridge style)""" + def __init__(self, north, east, south, west): # Input parameters are lists of cards ('Ah', '9s', etc) self.north = north @@ -163,6 +165,8 @@ behave like any existing field, so we'll subclass directly from from django.db import models class HandField(models.Field): + """A hand of cards (bridge style)""" + def __init__(self, *args, **kwargs): kwargs['max_length'] = 104 super(HandField, self).__init__(*args, **kwargs) @@ -244,6 +248,8 @@ simple: make sure your field subclass uses a special metaclass: For example:: class HandField(models.Field): + """A hand of cards (bridge style)""" + __metaclass__ = models.SubfieldBase def __init__(self, *args, **kwargs): @@ -252,6 +258,21 @@ For example:: This ensures that the :meth:`to_python` method, documented below, will always be called when the attribute is initialized. + +Documenting your Custom Field +----------------------------- + +As always, you should document your field type, so users will know what it is. +The best way to do this is to simply provide a docstring for it. This will +automatically be picked up by ``django.contrib.admindocs``, if you have it +installed, and the first line of it will show up as the field type in the +documentation for any model that uses your field. In the above examples, it +will show up as 'A hand of cards (bridge style)'. Note that if you provide a +more verbose docstring, only the first line will show up in +``django.contrib.admindocs``. The full docstring will, of course, still be +available through ``pydoc`` or the interactive interpreter's ``help()`` +function. + Useful methods -------------- diff --git a/docs/index.txt b/docs/index.txt index 4b14b74917..e6558a7478 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -28,7 +28,7 @@ Having trouble? We'd like to help! .. _archives of the django-users mailing list: http://groups.google.com/group/django-users/ .. _post a question: http://groups.google.com/group/django-users/ .. _#django IRC channel: irc://irc.freenode.net/django -.. _IRC logs: http://oebfare.com/logger/django/ +.. _IRC logs: http://botland.oebfare.com/logger/django/ .. _ticket tracker: http://code.djangoproject.com/ First steps diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index da9bb99224..f83fc37356 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -802,6 +802,14 @@ test <app or test identifier> Runs tests for all installed models. See :ref:`topics-testing` for more information. +--failfast +~~~~~~~~~~ + +.. versionadded:: 1.2 + +Use the ``--failfast`` option to stop running tests and report the failure +immediately after a test fails. + testserver <fixture fixture ...> -------------------------------- diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 7a2341f69b..26934f07a3 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -366,6 +366,36 @@ calls its ``as_table()`` method behind the scenes:: <tr><th><label for="id_sender">Sender:</label></th><td><input type="text" name="sender" id="id_sender" /></td></tr> <tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself" /></td></tr> +Styling required or erroneous form rows +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.2 + +It's pretty common to style form rows and fields that are required or have +errors. For example, you might want to present required form rows in bold and +highlight errors in red. + +The :class:`Form` class has a couple of hooks you can use to add ``class`` +attributes to required rows or to rows with errors: simple set the +:attr:`Form.error_css_class` and/or :attr:`Form.required_css_class` +attributes:: + + class ContactForm(Form): + error_css_class = 'error' + required_css_class = 'required' + + # ... and the rest of your fields here + +Once you've done that, rows will be given ``"error"`` and/or ``"required"`` +classes, as needed. The HTML will look something like:: + + >>> f = ContactForm(data) + >>> print f.as_table() + <tr class="required"><th><label for="id_subject">Subject:</label> ... + <tr class="required"><th><label for="id_message">Message:</label> ... + <tr class="required error"><th><label for="id_sender">Sender:</label> ... + <tr><th><label for="id_cc_myself">Cc myself:<label> ... + .. _ref-forms-api-configuring-label: Configuring HTML ``<label>`` tags diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index fd45e79876..0a5d04cdae 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -253,24 +253,30 @@ handler404 .. data:: handler404 -A string representing the full Python import path to the view that should be -called if none of the URL patterns match. +A callable, or a string representing the full Python import path to the view +that should be called if none of the URL patterns match. By default, this is ``'django.views.defaults.page_not_found'``. That default value should suffice. +.. versionchanged:: 1.2 + Previous versions of Django only accepted strings representing import paths. + handler500 ---------- .. data:: handler500 -A string representing the full Python import path to the view that should be -called in case of server errors. Server errors happen when you have runtime -errors in view code. +A callable, or a string representing the full Python import path to the view +that should be called in case of server errors. Server errors happen when you +have runtime errors in view code. By default, this is ``'django.views.defaults.server_error'``. That default value should suffice. +.. versionchanged:: 1.2 + Previous versions of Django only accepted strings representing import paths. + include ------- diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index cda3aa9d8e..d6f541bfd5 100644 --- a/tests/modeltests/model_formsets/models.py +++ b/tests/modeltests/model_formsets/models.py @@ -885,7 +885,7 @@ False >>> form = formset.forms[0] # this formset only has one form >>> now = form.fields['date_joined'].initial >>> print form.as_p() -<p><label for="id_membership_set-0-date_joined">Date joined:</label> <input type="text" name="membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /><input type="hidden" name="initial-membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /></p> +<p><label for="id_membership_set-0-date_joined">Date joined:</label> <input type="text" name="membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /><input type="hidden" name="initial-membership_set-0-date_joined" value="..." id="initial-membership_set-0-id_membership_set-0-date_joined" /></p> <p><label for="id_membership_set-0-karma">Karma:</label> <input type="text" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-person" value="1" id="id_membership_set-0-person" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p> # test for validation with callable defaults. Validations rely on hidden fields diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 167498ac37..3124071503 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -1157,7 +1157,6 @@ class AdminActionsTest(TestCase): self.assert_('action-checkbox-column' in response.content, "Expected an action-checkbox-column in response") - def test_multiple_actions_form(self): """ Test that actions come from the form whose submit button was pressed (#10618). @@ -1175,6 +1174,35 @@ class AdminActionsTest(TestCase): self.assertEquals(len(mail.outbox), 1) self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action') + def test_user_message_on_none_selected(self): + """ + User should see a warning when 'Go' is pressed and no items are selected. + """ + action_data = { + ACTION_CHECKBOX_NAME: [], + 'action' : 'delete_selected', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) + msg = """Items must be selected in order to perform actions on them. No items have been changed.""" + self.assertContains(response, msg) + self.failUnlessEqual(Subscriber.objects.count(), 2) + + def test_user_message_on_no_action(self): + """ + User should see a warning when 'Go' is pressed and no action is selected. + """ + action_data = { + ACTION_CHECKBOX_NAME: [1, 2], + 'action' : '', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) + msg = """No action selected.""" + self.assertContains(response, msg) + self.failUnlessEqual(Subscriber.objects.count(), 2) + + class TestInlineNotEditable(TestCase): fixtures = ['admin-views-users.xml'] diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py index 9d407d9ea7..c9736d38e1 100644 --- a/tests/regressiontests/forms/fields.py +++ b/tests/regressiontests/forms/fields.py @@ -1,17 +1,5 @@ # -*- coding: utf-8 -*- -tests = r""" ->>> from django.forms import * ->>> from django.forms.widgets import RadioFieldRenderer ->>> from django.core.files.uploadedfile import SimpleUploadedFile ->>> import datetime ->>> import time ->>> import re ->>> try: -... from decimal import Decimal -... except ImportError: -... from django.utils._decimal import Decimal - - +""" ########## # Fields # ########## @@ -35,1480 +23,825 @@ Each Field's __init__() takes at least these parameters: Other than that, the Field subclasses have class-specific options for __init__(). For example, CharField has a max_length option. - -# CharField ################################################################### - ->>> f = CharField() ->>> f.clean(1) -u'1' ->>> f.clean('hello') -u'hello' ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean([1, 2, 3]) -u'[1, 2, 3]' - ->>> f = CharField(required=False) ->>> f.clean(1) -u'1' ->>> f.clean('hello') -u'hello' ->>> f.clean(None) -u'' ->>> f.clean('') -u'' ->>> f.clean([1, 2, 3]) -u'[1, 2, 3]' - -CharField accepts an optional max_length parameter: ->>> f = CharField(max_length=10, required=False) ->>> f.clean('12345') -u'12345' ->>> f.clean('1234567890') -u'1234567890' ->>> f.clean('1234567890a') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at most 10 characters (it has 11).'] - -CharField accepts an optional min_length parameter: ->>> f = CharField(min_length=10, required=False) ->>> f.clean('') -u'' ->>> f.clean('12345') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at least 10 characters (it has 5).'] ->>> f.clean('1234567890') -u'1234567890' ->>> f.clean('1234567890a') -u'1234567890a' - ->>> f = CharField(min_length=10, required=True) ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('12345') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at least 10 characters (it has 5).'] ->>> f.clean('1234567890') -u'1234567890' ->>> f.clean('1234567890a') -u'1234567890a' - -# IntegerField ################################################################ - ->>> f = IntegerField() ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('1') -1 ->>> isinstance(f.clean('1'), int) -True ->>> f.clean('23') -23 ->>> f.clean('a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a whole number.'] ->>> f.clean(42) -42 ->>> f.clean(3.14) -Traceback (most recent call last): -... -ValidationError: [u'Enter a whole number.'] ->>> f.clean('1 ') -1 ->>> f.clean(' 1') -1 ->>> f.clean(' 1 ') -1 ->>> f.clean('1a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a whole number.'] - ->>> f = IntegerField(required=False) ->>> f.clean('') ->>> repr(f.clean('')) -'None' ->>> f.clean(None) ->>> repr(f.clean(None)) -'None' ->>> f.clean('1') -1 ->>> isinstance(f.clean('1'), int) -True ->>> f.clean('23') -23 ->>> f.clean('a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a whole number.'] ->>> f.clean('1 ') -1 ->>> f.clean(' 1') -1 ->>> f.clean(' 1 ') -1 ->>> f.clean('1a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a whole number.'] - -IntegerField accepts an optional max_value parameter: ->>> f = IntegerField(max_value=10) ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(1) -1 ->>> f.clean(10) -10 ->>> f.clean(11) -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is less than or equal to 10.'] ->>> f.clean('10') -10 ->>> f.clean('11') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is less than or equal to 10.'] - -IntegerField accepts an optional min_value parameter: ->>> f = IntegerField(min_value=10) ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(1) -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is greater than or equal to 10.'] ->>> f.clean(10) -10 ->>> f.clean(11) -11 ->>> f.clean('10') -10 ->>> f.clean('11') -11 - -min_value and max_value can be used together: ->>> f = IntegerField(min_value=10, max_value=20) ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(1) -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is greater than or equal to 10.'] ->>> f.clean(10) -10 ->>> f.clean(11) -11 ->>> f.clean('10') -10 ->>> f.clean('11') -11 ->>> f.clean(20) -20 ->>> f.clean(21) -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is less than or equal to 20.'] - -# FloatField ################################################################## - ->>> f = FloatField() ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('1') -1.0 ->>> isinstance(f.clean('1'), float) -True ->>> f.clean('23') -23.0 ->>> f.clean('3.14') -3.1400000000000001 ->>> f.clean(3.14) -3.1400000000000001 ->>> f.clean(42) -42.0 ->>> f.clean('a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a number.'] ->>> f.clean('1.0 ') -1.0 ->>> f.clean(' 1.0') -1.0 ->>> f.clean(' 1.0 ') -1.0 ->>> f.clean('1.0a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a number.'] - ->>> f = FloatField(required=False) ->>> f.clean('') - ->>> f.clean(None) - ->>> f.clean('1') -1.0 - -FloatField accepts min_value and max_value just like IntegerField: ->>> f = FloatField(max_value=1.5, min_value=0.5) - ->>> f.clean('1.6') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is less than or equal to 1.5.'] ->>> f.clean('0.4') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is greater than or equal to 0.5.'] ->>> f.clean('1.5') -1.5 ->>> f.clean('0.5') -0.5 - -# DecimalField ################################################################ - ->>> f = DecimalField(max_digits=4, decimal_places=2) ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('1') == Decimal("1") -True ->>> isinstance(f.clean('1'), Decimal) -True ->>> f.clean('23') == Decimal("23") -True ->>> f.clean('3.14') == Decimal("3.14") -True ->>> f.clean(3.14) == Decimal("3.14") -True ->>> f.clean(Decimal('3.14')) == Decimal("3.14") -True ->>> f.clean('a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a number.'] ->>> f.clean(u'łąść') -Traceback (most recent call last): -... -ValidationError: [u'Enter a number.'] ->>> f.clean('1.0 ') == Decimal("1.0") -True ->>> f.clean(' 1.0') == Decimal("1.0") -True ->>> f.clean(' 1.0 ') == Decimal("1.0") -True ->>> f.clean('1.0a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a number.'] ->>> f.clean('123.45') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 4 digits in total.'] ->>> f.clean('1.234') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 2 decimal places.'] ->>> f.clean('123.4') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 2 digits before the decimal point.'] ->>> f.clean('-12.34') == Decimal("-12.34") -True ->>> f.clean('-123.45') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 4 digits in total.'] ->>> f.clean('-.12') == Decimal("-0.12") -True ->>> f.clean('-00.12') == Decimal("-0.12") -True ->>> f.clean('-000.12') == Decimal("-0.12") -True ->>> f.clean('-000.123') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 2 decimal places.'] ->>> f.clean('-000.12345') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 4 digits in total.'] ->>> f.clean('--0.12') -Traceback (most recent call last): -... -ValidationError: [u'Enter a number.'] - ->>> f = DecimalField(max_digits=4, decimal_places=2, required=False) ->>> f.clean('') - ->>> f.clean(None) - ->>> f.clean('1') == Decimal("1") -True - -DecimalField accepts min_value and max_value just like IntegerField: ->>> f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5')) - ->>> f.clean('1.6') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is less than or equal to 1.5.'] ->>> f.clean('0.4') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value is greater than or equal to 0.5.'] ->>> f.clean('1.5') == Decimal("1.5") -True ->>> f.clean('0.5') == Decimal("0.5") -True ->>> f.clean('.5') == Decimal("0.5") -True ->>> f.clean('00.50') == Decimal("0.50") -True - - ->>> f = DecimalField(decimal_places=2) ->>> f.clean('0.00000001') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 2 decimal places.'] - - ->>> f = DecimalField(max_digits=3) - -# Leading whole zeros "collapse" to one digit. ->>> f.clean('0000000.10') == Decimal("0.1") -True - -# But a leading 0 before the . doesn't count towards max_digits ->>> f.clean('0000000.100') == Decimal("0.100") -True - -# Only leading whole zeros "collapse" to one digit. ->>> f.clean('000000.02') == Decimal('0.02') -True ->>> f.clean('000000.0002') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 3 digits in total.'] ->>> f.clean('.002') == Decimal("0.002") -True - ->>> f = DecimalField(max_digits=2, decimal_places=2) ->>> f.clean('.01') == Decimal(".01") -True ->>> f.clean('1.1') -Traceback (most recent call last): -... -ValidationError: [u'Ensure that there are no more than 0 digits before the decimal point.'] - - -# DateField ################################################################### - ->>> import datetime ->>> f = DateField() ->>> f.clean(datetime.date(2006, 10, 25)) -datetime.date(2006, 10, 25) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) -datetime.date(2006, 10, 25) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) -datetime.date(2006, 10, 25) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) -datetime.date(2006, 10, 25) ->>> f.clean('2006-10-25') -datetime.date(2006, 10, 25) ->>> f.clean('10/25/2006') -datetime.date(2006, 10, 25) ->>> f.clean('10/25/06') -datetime.date(2006, 10, 25) ->>> f.clean('Oct 25 2006') -datetime.date(2006, 10, 25) ->>> f.clean('October 25 2006') -datetime.date(2006, 10, 25) ->>> f.clean('October 25, 2006') -datetime.date(2006, 10, 25) ->>> f.clean('25 October 2006') -datetime.date(2006, 10, 25) ->>> f.clean('25 October, 2006') -datetime.date(2006, 10, 25) ->>> f.clean('2006-4-31') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] ->>> f.clean('200a-10-25') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] ->>> f.clean('25/10/06') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f = DateField(required=False) ->>> f.clean(None) ->>> repr(f.clean(None)) -'None' ->>> f.clean('') ->>> repr(f.clean('')) -'None' - -DateField accepts an optional input_formats parameter: ->>> f = DateField(input_formats=['%Y %m %d']) ->>> f.clean(datetime.date(2006, 10, 25)) -datetime.date(2006, 10, 25) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) -datetime.date(2006, 10, 25) ->>> f.clean('2006 10 25') -datetime.date(2006, 10, 25) - -The input_formats parameter overrides all default input formats, -so the default formats won't work unless you specify them: ->>> f.clean('2006-10-25') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] ->>> f.clean('10/25/2006') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] ->>> f.clean('10/25/06') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] - -# TimeField ################################################################### - ->>> import datetime ->>> f = TimeField() ->>> f.clean(datetime.time(14, 25)) -datetime.time(14, 25) ->>> f.clean(datetime.time(14, 25, 59)) -datetime.time(14, 25, 59) ->>> f.clean('14:25') -datetime.time(14, 25) ->>> f.clean('14:25:59') -datetime.time(14, 25, 59) ->>> f.clean('hello') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid time.'] ->>> f.clean('1:24 p.m.') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid time.'] - -TimeField accepts an optional input_formats parameter: ->>> f = TimeField(input_formats=['%I:%M %p']) ->>> f.clean(datetime.time(14, 25)) -datetime.time(14, 25) ->>> f.clean(datetime.time(14, 25, 59)) -datetime.time(14, 25, 59) ->>> f.clean('4:25 AM') -datetime.time(4, 25) ->>> f.clean('4:25 PM') -datetime.time(16, 25) - -The input_formats parameter overrides all default input formats, -so the default formats won't work unless you specify them: ->>> f.clean('14:30:45') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid time.'] - -# DateTimeField ############################################################### - ->>> import datetime ->>> f = DateTimeField() ->>> f.clean(datetime.date(2006, 10, 25)) -datetime.datetime(2006, 10, 25, 0, 0) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) -datetime.datetime(2006, 10, 25, 14, 30, 59) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) -datetime.datetime(2006, 10, 25, 14, 30, 59, 200) ->>> f.clean('2006-10-25 14:30:45') -datetime.datetime(2006, 10, 25, 14, 30, 45) ->>> f.clean('2006-10-25 14:30:00') -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean('2006-10-25 14:30') -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean('2006-10-25') -datetime.datetime(2006, 10, 25, 0, 0) ->>> f.clean('10/25/2006 14:30:45') -datetime.datetime(2006, 10, 25, 14, 30, 45) ->>> f.clean('10/25/2006 14:30:00') -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean('10/25/2006 14:30') -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean('10/25/2006') -datetime.datetime(2006, 10, 25, 0, 0) ->>> f.clean('10/25/06 14:30:45') -datetime.datetime(2006, 10, 25, 14, 30, 45) ->>> f.clean('10/25/06 14:30:00') -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean('10/25/06 14:30') -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean('10/25/06') -datetime.datetime(2006, 10, 25, 0, 0) ->>> f.clean('hello') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date/time.'] ->>> f.clean('2006-10-25 4:30 p.m.') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date/time.'] - -DateField accepts an optional input_formats parameter: ->>> f = DateTimeField(input_formats=['%Y %m %d %I:%M %p']) ->>> f.clean(datetime.date(2006, 10, 25)) -datetime.datetime(2006, 10, 25, 0, 0) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) -datetime.datetime(2006, 10, 25, 14, 30) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) -datetime.datetime(2006, 10, 25, 14, 30, 59) ->>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) -datetime.datetime(2006, 10, 25, 14, 30, 59, 200) ->>> f.clean('2006 10 25 2:30 PM') -datetime.datetime(2006, 10, 25, 14, 30) - -The input_formats parameter overrides all default input formats, -so the default formats won't work unless you specify them: ->>> f.clean('2006-10-25 14:30:45') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date/time.'] - ->>> f = DateTimeField(required=False) ->>> f.clean(None) ->>> repr(f.clean(None)) -'None' ->>> f.clean('') ->>> repr(f.clean('')) -'None' - -# RegexField ################################################################## - ->>> f = RegexField('^\d[A-F]\d$') ->>> f.clean('2A2') -u'2A2' ->>> f.clean('3F3') -u'3F3' ->>> f.clean('3G3') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] ->>> f.clean(' 2A2') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] ->>> f.clean('2A2 ') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f = RegexField('^\d[A-F]\d$', required=False) ->>> f.clean('2A2') -u'2A2' ->>> f.clean('3F3') -u'3F3' ->>> f.clean('3G3') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] ->>> f.clean('') -u'' - -Alternatively, RegexField can take a compiled regular expression: ->>> f = RegexField(re.compile('^\d[A-F]\d$')) ->>> f.clean('2A2') -u'2A2' ->>> f.clean('3F3') -u'3F3' ->>> f.clean('3G3') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] ->>> f.clean(' 2A2') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] ->>> f.clean('2A2 ') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] - -RegexField takes an optional error_message argument: ->>> f = RegexField('^\d\d\d\d$', error_message='Enter a four-digit number.') ->>> f.clean('1234') -u'1234' ->>> f.clean('123') -Traceback (most recent call last): -... -ValidationError: [u'Enter a four-digit number.'] ->>> f.clean('abcd') -Traceback (most recent call last): -... -ValidationError: [u'Enter a four-digit number.'] - -RegexField also access min_length and max_length parameters, for convenience. ->>> f = RegexField('^\d+$', min_length=5, max_length=10) ->>> f.clean('123') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at least 5 characters (it has 3).'] ->>> f.clean('abc') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at least 5 characters (it has 3).'] ->>> f.clean('12345') -u'12345' ->>> f.clean('1234567890') -u'1234567890' ->>> f.clean('12345678901') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at most 10 characters (it has 11).'] ->>> f.clean('12345a') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid value.'] - -# EmailField ################################################################## - ->>> f = EmailField() ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('person@example.com') -u'person@example.com' ->>> f.clean('foo') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('foo@') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('foo@bar') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('example@invalid-.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('example@-invalid.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('example@inv-.alid-.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('example@inv-.-alid.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('example@valid-----hyphens.com') -u'example@valid-----hyphens.com' - ->>> f.clean('example@valid-with-hyphens.com') -u'example@valid-with-hyphens.com' - -# Check for runaway regex security problem. This will take for-freeking-ever -# if the security fix isn't in place. ->>> f.clean('viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058') -Traceback (most recent call last): - ... -ValidationError: [u'Enter a valid e-mail address.'] - ->>> f = EmailField(required=False) ->>> f.clean('') -u'' ->>> f.clean(None) -u'' ->>> f.clean('person@example.com') -u'person@example.com' ->>> f.clean('foo') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('foo@') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('foo@bar') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] - -EmailField also access min_length and max_length parameters, for convenience. ->>> f = EmailField(min_length=10, max_length=15) ->>> f.clean('a@foo.com') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at least 10 characters (it has 9).'] ->>> f.clean('alf@foo.com') -u'alf@foo.com' ->>> f.clean('alf123456788@foo.com') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at most 15 characters (it has 20).'] - -# FileField ################################################################## - ->>> f = FileField() ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f.clean('', '') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f.clean('', 'files/test1.pdf') -'files/test1.pdf' - ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f.clean(None, '') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f.clean(None, 'files/test2.pdf') -'files/test2.pdf' - ->>> f.clean(SimpleUploadedFile('', '')) -Traceback (most recent call last): -... -ValidationError: [u'No file was submitted. Check the encoding type on the form.'] - ->>> f.clean(SimpleUploadedFile('', ''), '') -Traceback (most recent call last): -... -ValidationError: [u'No file was submitted. Check the encoding type on the form.'] - ->>> f.clean(None, 'files/test3.pdf') -'files/test3.pdf' - ->>> f.clean('some content that is not a file') -Traceback (most recent call last): -... -ValidationError: [u'No file was submitted. Check the encoding type on the form.'] - ->>> f.clean(SimpleUploadedFile('name', None)) -Traceback (most recent call last): -... -ValidationError: [u'The submitted file is empty.'] - ->>> f.clean(SimpleUploadedFile('name', '')) -Traceback (most recent call last): -... -ValidationError: [u'The submitted file is empty.'] - ->>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'))) -<class 'django.core.files.uploadedfile.SimpleUploadedFile'> - ->>> type(f.clean(SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह'))) -<class 'django.core.files.uploadedfile.SimpleUploadedFile'> - ->>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf')) -<class 'django.core.files.uploadedfile.SimpleUploadedFile'> - ->>> f = FileField(max_length = 5) ->>> f.clean(SimpleUploadedFile('test_maxlength.txt', 'hello world')) -Traceback (most recent call last): -... -ValidationError: [u'Ensure this filename has at most 5 characters (it has 18).'] - ->>> f.clean('', 'files/test1.pdf') -'files/test1.pdf' - ->>> f.clean(None, 'files/test2.pdf') -'files/test2.pdf' - ->>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'))) -<class 'django.core.files.uploadedfile.SimpleUploadedFile'> - -# URLField ################################################################## - ->>> f = URLField() ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('http://localhost') -u'http://localhost/' ->>> f.clean('http://example.com') -u'http://example.com/' ->>> f.clean('http://www.example.com') -u'http://www.example.com/' ->>> f.clean('http://www.example.com:8000/test') -u'http://www.example.com:8000/test' ->>> f.clean('valid-with-hyphens.com') -u'http://valid-with-hyphens.com/' ->>> f.clean('subdomain.domain.com') -u'http://subdomain.domain.com/' ->>> f.clean('http://200.8.9.10') -u'http://200.8.9.10/' ->>> f.clean('http://200.8.9.10:8000/test') -u'http://200.8.9.10:8000/test' ->>> f.clean('foo') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://example') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://example.') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://invalid-.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://-invalid.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://inv-.alid-.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://inv-.-alid.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://valid-----hyphens.com') -u'http://valid-----hyphens.com/' - ->>> f = URLField(required=False) ->>> f.clean('') -u'' ->>> f.clean(None) -u'' ->>> f.clean('http://example.com') -u'http://example.com/' ->>> f.clean('http://www.example.com') -u'http://www.example.com/' ->>> f.clean('foo') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://example') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://example.') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('.') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('com.') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://example.com.') -u'http://example.com./' ->>> f.clean('example.com.') -u'http://example.com./' - -# hangs "forever" if catastrophic backtracking in ticket:#11198 not fixed ->>> f.clean('http://%s' % ("X"*200,)) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] - -# a second test, to make sure the problem is really addressed, even on -# domains that don't fail the domain label length check in the regex ->>> f.clean('http://%s' % ("X"*60,)) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] - ->>> f.clean('http://.com') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] - -URLField takes an optional verify_exists parameter, which is False by default. -This verifies that the URL is live on the Internet and doesn't return a 404 or 500: ->>> f = URLField(verify_exists=True) ->>> f.clean('http://www.google.com') # This will fail if there's no Internet connection -u'http://www.google.com/' ->>> f.clean('http://example') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://www.broken.djangoproject.com') # bad domain -Traceback (most recent call last): -... -ValidationError: [u'This URL appears to be a broken link.'] ->>> f.clean('http://google.com/we-love-microsoft.html') # good domain, bad page -Traceback (most recent call last): -... -ValidationError: [u'This URL appears to be a broken link.'] ->>> f = URLField(verify_exists=True, required=False) ->>> f.clean('') -u'' ->>> f.clean('http://www.google.com') # This will fail if there's no Internet connection -u'http://www.google.com/' - -URLField also access min_length and max_length parameters, for convenience. ->>> f = URLField(min_length=15, max_length=20) ->>> f.clean('http://f.com') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at least 15 characters (it has 13).'] ->>> f.clean('http://example.com') -u'http://example.com/' ->>> f.clean('http://abcdefghijklmnopqrstuvwxyz.com') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at most 20 characters (it has 38).'] - -URLField should prepend 'http://' if no scheme was given ->>> f = URLField(required=False) ->>> f.clean('example.com') -u'http://example.com/' ->>> f.clean('') -u'' ->>> f.clean('https://example.com') -u'https://example.com/' - -URLField should append '/' if no path was given ->>> f = URLField() ->>> f.clean('http://example.com') -u'http://example.com/' - -URLField shouldn't change the path if it was given ->>> f.clean('http://example.com/test') -u'http://example.com/test' - -# BooleanField ################################################################ - ->>> f = BooleanField() ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(True) -True ->>> f.clean(False) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(1) -True ->>> f.clean(0) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('Django rocks') -True - ->>> f.clean('True') -True ->>> f.clean('False') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f = BooleanField(required=False) ->>> f.clean('') -False ->>> f.clean(None) -False ->>> f.clean(True) -True ->>> f.clean(False) -False ->>> f.clean(1) -True ->>> f.clean(0) -False ->>> f.clean('1') -True ->>> f.clean('0') -False ->>> f.clean('Django rocks') -True - -A form's BooleanField with a hidden widget will output the string 'False', so -that should clean to the boolean value False: ->>> f.clean('False') -False - -# ChoiceField ################################################################# - ->>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')]) ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(1) -u'1' ->>> f.clean('1') -u'1' ->>> f.clean('3') -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] - ->>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) ->>> f.clean('') -u'' ->>> f.clean(None) -u'' ->>> f.clean(1) -u'1' ->>> f.clean('1') -u'1' ->>> f.clean('3') -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] - ->>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) ->>> f.clean('J') -u'J' ->>> f.clean('John') -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. John is not one of the available choices.'] - ->>> f = ChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) ->>> f.clean(1) -u'1' ->>> f.clean('1') -u'1' ->>> f.clean(3) -u'3' ->>> f.clean('3') -u'3' ->>> f.clean(5) -u'5' ->>> f.clean('5') -u'5' ->>> f.clean('6') -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] - -# TypedChoiceField ############################################################ - -# TypedChoiceField is just like ChoiceField, except that coerced types will -# be returned: ->>> f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int) ->>> f.clean('1') -1 ->>> f.clean('2') -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 2 is not one of the available choices.'] - -# Different coercion, same validation. ->>> f.coerce = float ->>> f.clean('1') -1.0 - - -# This can also cause weirdness: be careful (bool(-1) == True, remember) ->>> f.coerce = bool ->>> f.clean('-1') -True - -# Even more weirdness: if you have a valid choice but your coercion function -# can't coerce, you'll still get a validation error. Don't do this! ->>> f = TypedChoiceField(choices=[('A', 'A'), ('B', 'B')], coerce=int) ->>> f.clean('B') -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. B is not one of the available choices.'] - -# Required fields require values ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - -# Non-required fields aren't required ->>> f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False) ->>> f.clean('') -'' - -# If you want cleaning an empty value to return a different type, tell the field ->>> f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False, empty_value=None) ->>> print f.clean('') -None - -# NullBooleanField ############################################################ - ->>> f = NullBooleanField() ->>> f.clean('') ->>> f.clean(True) -True ->>> f.clean(False) -False ->>> f.clean(None) ->>> f.clean('0') -False ->>> f.clean('1') -True ->>> f.clean('2') ->>> f.clean('3') ->>> f.clean('hello') - -# Make sure that the internal value is preserved if using HiddenInput (#7753) ->>> class HiddenNullBooleanForm(Form): -... hidden_nullbool1 = NullBooleanField(widget=HiddenInput, initial=True) -... hidden_nullbool2 = NullBooleanField(widget=HiddenInput, initial=False) ->>> f = HiddenNullBooleanForm() ->>> print f -<input type="hidden" name="hidden_nullbool1" value="True" id="id_hidden_nullbool1" /><input type="hidden" name="hidden_nullbool2" value="False" id="id_hidden_nullbool2" /> ->>> f = HiddenNullBooleanForm({ 'hidden_nullbool1': 'True', 'hidden_nullbool2': 'False' }) ->>> f.full_clean() ->>> f.cleaned_data['hidden_nullbool1'] -True ->>> f.cleaned_data['hidden_nullbool2'] -False - -# Make sure we're compatible with MySQL, which uses 0 and 1 for its boolean -# values. (#9609) ->>> NULLBOOL_CHOICES = (('1', 'Yes'), ('0', 'No'), ('', 'Unknown')) ->>> class MySQLNullBooleanForm(Form): -... nullbool0 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) -... nullbool1 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) -... nullbool2 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) ->>> f = MySQLNullBooleanForm({ 'nullbool0': '1', 'nullbool1': '0', 'nullbool2': '' }) ->>> f.full_clean() ->>> f.cleaned_data['nullbool0'] -True ->>> f.cleaned_data['nullbool1'] -False ->>> f.cleaned_data['nullbool2'] - -# MultipleChoiceField ######################################################### - ->>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')]) ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean([1]) -[u'1'] ->>> f.clean(['1']) -[u'1'] ->>> f.clean(['1', '2']) -[u'1', u'2'] ->>> f.clean([1, '2']) -[u'1', u'2'] ->>> f.clean((1, '2')) -[u'1', u'2'] ->>> f.clean('hello') -Traceback (most recent call last): -... -ValidationError: [u'Enter a list of values.'] ->>> f.clean([]) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(()) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(['3']) -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] - ->>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) ->>> f.clean('') -[] ->>> f.clean(None) -[] ->>> f.clean([1]) -[u'1'] ->>> f.clean(['1']) -[u'1'] ->>> f.clean(['1', '2']) -[u'1', u'2'] ->>> f.clean([1, '2']) -[u'1', u'2'] ->>> f.clean((1, '2')) -[u'1', u'2'] ->>> f.clean('hello') -Traceback (most recent call last): -... -ValidationError: [u'Enter a list of values.'] ->>> f.clean([]) -[] ->>> f.clean(()) -[] ->>> f.clean(['3']) -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] - ->>> f = MultipleChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) ->>> f.clean([1]) -[u'1'] ->>> f.clean(['1']) -[u'1'] ->>> f.clean([1, 5]) -[u'1', u'5'] ->>> f.clean([1, '5']) -[u'1', u'5'] ->>> f.clean(['1', 5]) -[u'1', u'5'] ->>> f.clean(['1', '5']) -[u'1', u'5'] ->>> f.clean(['6']) -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] ->>> f.clean(['1','6']) -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] - - -# ComboField ################################################################## - -ComboField takes a list of fields that should be used to validate a value, -in that order. ->>> f = ComboField(fields=[CharField(max_length=20), EmailField()]) ->>> f.clean('test@example.com') -u'test@example.com' ->>> f.clean('longemailaddress@example.com') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at most 20 characters (it has 28).'] ->>> f.clean('not an e-mail') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] - ->>> f = ComboField(fields=[CharField(max_length=20), EmailField()], required=False) ->>> f.clean('test@example.com') -u'test@example.com' ->>> f.clean('longemailaddress@example.com') -Traceback (most recent call last): -... -ValidationError: [u'Ensure this value has at most 20 characters (it has 28).'] ->>> f.clean('not an e-mail') -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid e-mail address.'] ->>> f.clean('') -u'' ->>> f.clean(None) -u'' - -# FilePathField ############################################################### - ->>> def fix_os_paths(x): -... if isinstance(x, basestring): -... return x.replace('\\', '/') -... elif isinstance(x, tuple): -... return tuple(fix_os_paths(list(x))) -... elif isinstance(x, list): -... return [fix_os_paths(y) for y in x] -... else: -... return x -... ->>> import os ->>> from django import forms ->>> path = forms.__file__ ->>> path = os.path.dirname(path) + '/' ->>> fix_os_paths(path) -'.../django/forms/' ->>> f = forms.FilePathField(path=path) ->>> f.choices = [p for p in f.choices if p[0].endswith('.py')] ->>> f.choices.sort() ->>> fix_os_paths(f.choices) -[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/widgets.py', 'widgets.py')] ->>> f.clean('fields.py') -Traceback (most recent call last): -... -ValidationError: [u'Select a valid choice. fields.py is not one of the available choices.'] ->>> fix_os_paths(f.clean(path + 'fields.py')) -u'.../django/forms/fields.py' ->>> f = forms.FilePathField(path=path, match='^.*?\.py$') ->>> f.choices.sort() ->>> fix_os_paths(f.choices) -[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/widgets.py', 'widgets.py')] ->>> f = forms.FilePathField(path=path, recursive=True, match='^.*?\.py$') ->>> f.choices.sort() ->>> fix_os_paths(f.choices) -[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/extras/__init__.py', 'extras/__init__.py'), ('.../django/forms/extras/widgets.py', 'extras/widgets.py'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/widgets.py', 'widgets.py')] - -# SplitDateTimeField ########################################################## - ->>> f = SplitDateTimeField() ->>> f.widget -<django.forms.widgets.SplitDateTimeWidget object ... ->>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)]) -datetime.datetime(2006, 1, 10, 7, 30) ->>> f.clean(None) -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('') -Traceback (most recent call last): -... -ValidationError: [u'This field is required.'] ->>> f.clean('hello') -Traceback (most recent call last): -... -ValidationError: [u'Enter a list of values.'] ->>> f.clean(['hello', 'there']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.', u'Enter a valid time.'] ->>> f.clean(['2006-01-10', 'there']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid time.'] ->>> f.clean(['hello', '07:30']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] - ->>> f = SplitDateTimeField(required=False) ->>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)]) -datetime.datetime(2006, 1, 10, 7, 30) ->>> f.clean(['2006-01-10', '07:30']) -datetime.datetime(2006, 1, 10, 7, 30) ->>> f.clean(None) ->>> f.clean('') ->>> f.clean(['']) ->>> f.clean(['', '']) ->>> f.clean('hello') -Traceback (most recent call last): -... -ValidationError: [u'Enter a list of values.'] ->>> f.clean(['hello', 'there']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.', u'Enter a valid time.'] ->>> f.clean(['2006-01-10', 'there']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid time.'] ->>> f.clean(['hello', '07:30']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] ->>> f.clean(['2006-01-10', '']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid time.'] ->>> f.clean(['2006-01-10']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid time.'] ->>> f.clean(['', '07:30']) -Traceback (most recent call last): -... -ValidationError: [u'Enter a valid date.'] """ +import datetime +import time +import re +import os + +from unittest import TestCase + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.forms import * +from django.forms.widgets import RadioFieldRenderer + +try: + from decimal import Decimal +except ImportError: + from django.utils._decimal import Decimal + + +def fix_os_paths(x): + if isinstance(x, basestring): + return x.replace('\\', '/') + elif isinstance(x, tuple): + return tuple(fix_os_paths(list(x))) + elif isinstance(x, list): + return [fix_os_paths(y) for y in x] + else: + return x + + +class FieldsTests(TestCase): + + def assertRaisesErrorWithMessage(self, error, message, callable, *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + # CharField ################################################################### + + def test_charfield_0(self): + f = CharField() + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'hello', f.clean('hello')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertEqual(u'[1, 2, 3]', f.clean([1, 2, 3])) + + def test_charfield_1(self): + f = CharField(required=False) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'hello', f.clean('hello')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'[1, 2, 3]', f.clean([1, 2, 3])) + + def test_charfield_2(self): + f = CharField(max_length=10, required=False) + self.assertEqual(u'12345', f.clean('12345')) + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 10 characters (it has 11).']", f.clean, '1234567890a') + + def test_charfield_3(self): + f = CharField(min_length=10, required=False) + self.assertEqual(u'', f.clean('')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 10 characters (it has 5).']", f.clean, '12345') + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertEqual(u'1234567890a', f.clean('1234567890a')) + + def test_charfield_4(self): + f = CharField(min_length=10, required=True) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 10 characters (it has 5).']", f.clean, '12345') + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertEqual(u'1234567890a', f.clean('1234567890a')) + + # IntegerField ################################################################ + + def test_integerfield_5(self): + f = IntegerField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(1, f.clean('1')) + self.assertEqual(True, isinstance(f.clean('1'), int)) + self.assertEqual(23, f.clean('23')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, 'a') + self.assertEqual(42, f.clean(42)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, 3.14) + self.assertEqual(1, f.clean('1 ')) + self.assertEqual(1, f.clean(' 1')) + self.assertEqual(1, f.clean(' 1 ')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, '1a') + + def test_integerfield_6(self): + f = IntegerField(required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual('None', repr(f.clean(''))) + self.assertEqual(None, f.clean(None)) + self.assertEqual('None', repr(f.clean(None))) + self.assertEqual(1, f.clean('1')) + self.assertEqual(True, isinstance(f.clean('1'), int)) + self.assertEqual(23, f.clean('23')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, 'a') + self.assertEqual(1, f.clean('1 ')) + self.assertEqual(1, f.clean(' 1')) + self.assertEqual(1, f.clean(' 1 ')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, '1a') + + def test_integerfield_7(self): + f = IntegerField(max_value=10) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(1, f.clean(1)) + self.assertEqual(10, f.clean(10)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 10.']", f.clean, 11) + self.assertEqual(10, f.clean('10')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 10.']", f.clean, '11') + + def test_integerfield_8(self): + f = IntegerField(min_value=10) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 10.']", f.clean, 1) + self.assertEqual(10, f.clean(10)) + self.assertEqual(11, f.clean(11)) + self.assertEqual(10, f.clean('10')) + self.assertEqual(11, f.clean('11')) + + def test_integerfield_9(self): + f = IntegerField(min_value=10, max_value=20) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 10.']", f.clean, 1) + self.assertEqual(10, f.clean(10)) + self.assertEqual(11, f.clean(11)) + self.assertEqual(10, f.clean('10')) + self.assertEqual(11, f.clean('11')) + self.assertEqual(20, f.clean(20)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 20.']", f.clean, 21) + + # FloatField ################################################################## + + def test_floatfield_10(self): + f = FloatField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(1.0, f.clean('1')) + self.assertEqual(True, isinstance(f.clean('1'), float)) + self.assertEqual(23.0, f.clean('23')) + self.assertEqual(3.1400000000000001, f.clean('3.14')) + self.assertEqual(3.1400000000000001, f.clean(3.14)) + self.assertEqual(42.0, f.clean(42)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, 'a') + self.assertEqual(1.0, f.clean('1.0 ')) + self.assertEqual(1.0, f.clean(' 1.0')) + self.assertEqual(1.0, f.clean(' 1.0 ')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, '1.0a') + + def test_floatfield_11(self): + f = FloatField(required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(1.0, f.clean('1')) + + def test_floatfield_12(self): + f = FloatField(max_value=1.5, min_value=0.5) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 1.5.']", f.clean, '1.6') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 0.5.']", f.clean, '0.4') + self.assertEqual(1.5, f.clean('1.5')) + self.assertEqual(0.5, f.clean('0.5')) + + # DecimalField ################################################################ + + def test_decimalfield_13(self): + f = DecimalField(max_digits=4, decimal_places=2) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(f.clean('1'), Decimal("1")) + self.assertEqual(True, isinstance(f.clean('1'), Decimal)) + self.assertEqual(f.clean('23'), Decimal("23")) + self.assertEqual(f.clean('3.14'), Decimal("3.14")) + self.assertEqual(f.clean(3.14), Decimal("3.14")) + self.assertEqual(f.clean(Decimal('3.14')), Decimal("3.14")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, 'a') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, u'łąść') + self.assertEqual(f.clean('1.0 '), Decimal("1.0")) + self.assertEqual(f.clean(' 1.0'), Decimal("1.0")) + self.assertEqual(f.clean(' 1.0 '), Decimal("1.0")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, '1.0a') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 4 digits in total.']", f.clean, '123.45') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 decimal places.']", f.clean, '1.234') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 digits before the decimal point.']", f.clean, '123.4') + self.assertEqual(f.clean('-12.34'), Decimal("-12.34")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 4 digits in total.']", f.clean, '-123.45') + self.assertEqual(f.clean('-.12'), Decimal("-0.12")) + self.assertEqual(f.clean('-00.12'), Decimal("-0.12")) + self.assertEqual(f.clean('-000.12'), Decimal("-0.12")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 decimal places.']", f.clean, '-000.123') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 4 digits in total.']", f.clean, '-000.12345') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, '--0.12') + + def test_decimalfield_14(self): + f = DecimalField(max_digits=4, decimal_places=2, required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(f.clean('1'), Decimal("1")) + + def test_decimalfield_15(self): + f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 1.5.']", f.clean, '1.6') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 0.5.']", f.clean, '0.4') + self.assertEqual(f.clean('1.5'), Decimal("1.5")) + self.assertEqual(f.clean('0.5'), Decimal("0.5")) + self.assertEqual(f.clean('.5'), Decimal("0.5")) + self.assertEqual(f.clean('00.50'), Decimal("0.50")) + + def test_decimalfield_16(self): + f = DecimalField(decimal_places=2) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 decimal places.']", f.clean, '0.00000001') + + def test_decimalfield_17(self): + f = DecimalField(max_digits=3) + # Leading whole zeros "collapse" to one digit. + self.assertEqual(f.clean('0000000.10'), Decimal("0.1")) + # But a leading 0 before the . doesn't count towards max_digits + self.assertEqual(f.clean('0000000.100'), Decimal("0.100")) + # Only leading whole zeros "collapse" to one digit. + self.assertEqual(f.clean('000000.02'), Decimal('0.02')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 3 digits in total.']", f.clean, '000000.0002') + self.assertEqual(f.clean('.002'), Decimal("0.002")) + + def test_decimalfield_18(self): + f = DecimalField(max_digits=2, decimal_places=2) + self.assertEqual(f.clean('.01'), Decimal(".01")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 0 digits before the decimal point.']", f.clean, '1.1') + + # DateField ################################################################### + + def test_datefield_19(self): + f = DateField() + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('2006-10-25')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('10/25/2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('10/25/06')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('Oct 25 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('October 25 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('October 25, 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('25 October 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('25 October, 2006')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '2006-4-31') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '200a-10-25') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '25/10/06') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + + def test_datefield_20(self): + f = DateField(required=False) + self.assertEqual(None, f.clean(None)) + self.assertEqual('None', repr(f.clean(None))) + self.assertEqual(None, f.clean('')) + self.assertEqual('None', repr(f.clean(''))) + + def test_datefield_21(self): + f = DateField(input_formats=['%Y %m %d']) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('2006 10 25')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '2006-10-25') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '10/25/2006') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '10/25/06') + + # TimeField ################################################################### + + def test_timefield_22(self): + f = TimeField() + self.assertEqual(datetime.time(14, 25), f.clean(datetime.time(14, 25))) + self.assertEqual(datetime.time(14, 25, 59), f.clean(datetime.time(14, 25, 59))) + self.assertEqual(datetime.time(14, 25), f.clean('14:25')) + self.assertEqual(datetime.time(14, 25, 59), f.clean('14:25:59')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, '1:24 p.m.') + + def test_timefield_23(self): + f = TimeField(input_formats=['%I:%M %p']) + self.assertEqual(datetime.time(14, 25), f.clean(datetime.time(14, 25))) + self.assertEqual(datetime.time(14, 25, 59), f.clean(datetime.time(14, 25, 59))) + self.assertEqual(datetime.time(4, 25), f.clean('4:25 AM')) + self.assertEqual(datetime.time(16, 25), f.clean('4:25 PM')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, '14:30:45') + + # DateTimeField ############################################################### + + def test_datetimefield_24(self): + f = DateTimeField() + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59, 200), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45), f.clean('2006-10-25 14:30:45')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('2006-10-25 14:30:00')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('2006-10-25 14:30')) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean('2006-10-25')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45), f.clean('10/25/2006 14:30:45')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/2006 14:30:00')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/2006 14:30')) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean('10/25/2006')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45), f.clean('10/25/06 14:30:45')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/06 14:30:00')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/06 14:30')) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean('10/25/06')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date/time.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date/time.']", f.clean, '2006-10-25 4:30 p.m.') + + def test_datetimefield_25(self): + f = DateTimeField(input_formats=['%Y %m %d %I:%M %p']) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59, 200), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('2006 10 25 2:30 PM')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date/time.']", f.clean, '2006-10-25 14:30:45') + + def test_datetimefield_26(self): + f = DateTimeField(required=False) + self.assertEqual(None, f.clean(None)) + self.assertEqual('None', repr(f.clean(None))) + self.assertEqual(None, f.clean('')) + self.assertEqual('None', repr(f.clean(''))) + + # RegexField ################################################################## + + def test_regexfield_27(self): + f = RegexField('^\d[A-F]\d$') + self.assertEqual(u'2A2', f.clean('2A2')) + self.assertEqual(u'3F3', f.clean('3F3')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '3G3') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, ' 2A2') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '2A2 ') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + + def test_regexfield_28(self): + f = RegexField('^\d[A-F]\d$', required=False) + self.assertEqual(u'2A2', f.clean('2A2')) + self.assertEqual(u'3F3', f.clean('3F3')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '3G3') + self.assertEqual(u'', f.clean('')) + + def test_regexfield_29(self): + f = RegexField(re.compile('^\d[A-F]\d$')) + self.assertEqual(u'2A2', f.clean('2A2')) + self.assertEqual(u'3F3', f.clean('3F3')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '3G3') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, ' 2A2') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '2A2 ') + + def test_regexfield_30(self): + f = RegexField('^\d\d\d\d$', error_message='Enter a four-digit number.') + self.assertEqual(u'1234', f.clean('1234')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a four-digit number.']", f.clean, '123') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a four-digit number.']", f.clean, 'abcd') + + def test_regexfield_31(self): + f = RegexField('^\d+$', min_length=5, max_length=10) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).']", f.clean, '123') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).']", f.clean, 'abc') + self.assertEqual(u'12345', f.clean('12345')) + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 10 characters (it has 11).']", f.clean, '12345678901') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '12345a') + + # EmailField ################################################################## + + def test_emailfield_32(self): + f = EmailField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(u'person@example.com', f.clean('person@example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@bar') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@invalid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@-invalid.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@inv-.alid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@inv-.-alid.com') + self.assertEqual(u'example@valid-----hyphens.com', f.clean('example@valid-----hyphens.com')) + self.assertEqual(u'example@valid-with-hyphens.com', f.clean('example@valid-with-hyphens.com')) + + def test_email_regexp_for_performance(self): + f = EmailField() + # Check for runaway regex security problem. This will take for-freeking-ever + # if the security fix isn't in place. + self.assertRaisesErrorWithMessage( + ValidationError, + "[u'Enter a valid e-mail address.']", + f.clean, + 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' + ) + + def test_emailfield_33(self): + f = EmailField(required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'person@example.com', f.clean('person@example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@bar') + + def test_emailfield_34(self): + f = EmailField(min_length=10, max_length=15) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 10 characters (it has 9).']", f.clean, 'a@foo.com') + self.assertEqual(u'alf@foo.com', f.clean('alf@foo.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 15 characters (it has 20).']", f.clean, 'alf123456788@foo.com') + + # FileField ################################################################## + + def test_filefield_35(self): + f = FileField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '', '') + self.assertEqual('files/test1.pdf', f.clean('', 'files/test1.pdf')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None, '') + self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', '')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', ''), '') + self.assertEqual('files/test3.pdf', f.clean(None, 'files/test3.pdf')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, 'some content that is not a file') + self.assertRaisesErrorWithMessage(ValidationError, "[u'The submitted file is empty.']", f.clean, SimpleUploadedFile('name', None)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'The submitted file is empty.']", f.clean, SimpleUploadedFile('name', '')) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content')))) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')))) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf'))) + + def test_filefield_36(self): + f = FileField(max_length = 5) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this filename has at most 5 characters (it has 18).']", f.clean, SimpleUploadedFile('test_maxlength.txt', 'hello world')) + self.assertEqual('files/test1.pdf', f.clean('', 'files/test1.pdf')) + self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf')) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content')))) + + # URLField ################################################################## + + def test_urlfield_37(self): + f = URLField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(u'http://localhost/', f.clean('http://localhost')) + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertEqual(u'http://example.com./', f.clean('http://example.com.')) + self.assertEqual(u'http://www.example.com/', f.clean('http://www.example.com')) + self.assertEqual(u'http://www.example.com:8000/test', f.clean('http://www.example.com:8000/test')) + self.assertEqual(u'http://valid-with-hyphens.com/', f.clean('valid-with-hyphens.com')) + self.assertEqual(u'http://subdomain.domain.com/', f.clean('subdomain.domain.com')) + self.assertEqual(u'http://200.8.9.10/', f.clean('http://200.8.9.10')) + self.assertEqual(u'http://200.8.9.10:8000/test', f.clean('http://200.8.9.10:8000/test')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'com.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, '.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://invalid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://-invalid.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://inv-.alid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://inv-.-alid.com') + self.assertEqual(u'http://valid-----hyphens.com/', f.clean('http://valid-----hyphens.com')) + + def test_url_regex_ticket11198(self): + f = URLField() + # hangs "forever" if catastrophic backtracking in ticket:#11198 not fixed + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://%s' % ("X"*200,)) + + # a second test, to make sure the problem is really addressed, even on + # domains that don't fail the domain label length check in the regex + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://%s' % ("X"*60,)) + + def test_urlfield_38(self): + f = URLField(required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertEqual(u'http://www.example.com/', f.clean('http://www.example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://.com') + + def test_urlfield_39(self): + f = URLField(verify_exists=True) + self.assertEqual(u'http://www.google.com/', f.clean('http://www.google.com')) # This will fail if there's no Internet connection + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example') + self.assertRaises(ValidationError, f.clean, 'http://www.broken.djangoproject.com') # bad domain + try: + f.clean('http://www.broken.djangoproject.com') # bad domain + except ValidationError, e: + self.assertEqual("[u'This URL appears to be a broken link.']", str(e)) + self.assertRaises(ValidationError, f.clean, 'http://google.com/we-love-microsoft.html') # good domain, bad page + try: + f.clean('http://google.com/we-love-microsoft.html') # good domain, bad page + except ValidationError, e: + self.assertEqual("[u'This URL appears to be a broken link.']", str(e)) + + def test_urlfield_40(self): + f = URLField(verify_exists=True, required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'http://www.google.com/', f.clean('http://www.google.com')) # This will fail if there's no Internet connection + + def test_urlfield_41(self): + f = URLField(min_length=15, max_length=20) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 15 characters (it has 13).']", f.clean, 'http://f.com') + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 20 characters (it has 38).']", f.clean, 'http://abcdefghijklmnopqrstuvwxyz.com') + + def test_urlfield_42(self): + f = URLField(required=False) + self.assertEqual(u'http://example.com/', f.clean('example.com')) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'https://example.com/', f.clean('https://example.com')) + + def test_urlfield_43(self): + f = URLField() + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertEqual(u'http://example.com/test', f.clean('http://example.com/test')) + + # BooleanField ################################################################ + + def test_booleanfield_44(self): + f = BooleanField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(True, f.clean(True)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, False) + self.assertEqual(True, f.clean(1)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, 0) + self.assertEqual(True, f.clean('Django rocks')) + self.assertEqual(True, f.clean('True')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, 'False') + + def test_booleanfield_45(self): + f = BooleanField(required=False) + self.assertEqual(False, f.clean('')) + self.assertEqual(False, f.clean(None)) + self.assertEqual(True, f.clean(True)) + self.assertEqual(False, f.clean(False)) + self.assertEqual(True, f.clean(1)) + self.assertEqual(False, f.clean(0)) + self.assertEqual(True, f.clean('1')) + self.assertEqual(False, f.clean('0')) + self.assertEqual(True, f.clean('Django rocks')) + self.assertEqual(False, f.clean('False')) + + # ChoiceField ################################################################# + + def test_choicefield_46(self): + f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')]) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'1', f.clean('1')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, '3') + + def test_choicefield_47(self): + f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'1', f.clean('1')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, '3') + + def test_choicefield_48(self): + f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) + self.assertEqual(u'J', f.clean('J')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. John is not one of the available choices.']", f.clean, 'John') + + def test_choicefield_49(self): + f = ChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'1', f.clean('1')) + self.assertEqual(u'3', f.clean(3)) + self.assertEqual(u'3', f.clean('3')) + self.assertEqual(u'5', f.clean(5)) + self.assertEqual(u'5', f.clean('5')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, '6') + + # TypedChoiceField ############################################################ + # TypedChoiceField is just like ChoiceField, except that coerced types will + # be returned: + + def test_typedchoicefield_50(self): + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int) + self.assertEqual(1, f.clean('1')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 2 is not one of the available choices.']", f.clean, '2') + + def test_typedchoicefield_51(self): + # Different coercion, same validation. + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=float) + self.assertEqual(1.0, f.clean('1')) + + def test_typedchoicefield_52(self): + # This can also cause weirdness: be careful (bool(-1) == True, remember) + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=bool) + self.assertEqual(True, f.clean('-1')) + + def test_typedchoicefield_53(self): + # Even more weirdness: if you have a valid choice but your coercion function + # can't coerce, you'll still get a validation error. Don't do this! + f = TypedChoiceField(choices=[('A', 'A'), ('B', 'B')], coerce=int) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. B is not one of the available choices.']", f.clean, 'B') + # Required fields require values + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + + def test_typedchoicefield_54(self): + # Non-required fields aren't required + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False) + self.assertEqual('', f.clean('')) + # If you want cleaning an empty value to return a different type, tell the field + + def test_typedchoicefield_55(self): + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False, empty_value=None) + self.assertEqual(None, f.clean('')) + + # NullBooleanField ############################################################ + + def test_nullbooleanfield_56(self): + f = NullBooleanField() + self.assertEqual(None, f.clean('')) + self.assertEqual(True, f.clean(True)) + self.assertEqual(False, f.clean(False)) + self.assertEqual(None, f.clean(None)) + self.assertEqual(False, f.clean('0')) + self.assertEqual(True, f.clean('1')) + self.assertEqual(None, f.clean('2')) + self.assertEqual(None, f.clean('3')) + self.assertEqual(None, f.clean('hello')) + + + def test_nullbooleanfield_57(self): + # Make sure that the internal value is preserved if using HiddenInput (#7753) + class HiddenNullBooleanForm(Form): + hidden_nullbool1 = NullBooleanField(widget=HiddenInput, initial=True) + hidden_nullbool2 = NullBooleanField(widget=HiddenInput, initial=False) + f = HiddenNullBooleanForm() + self.assertEqual('<input type="hidden" name="hidden_nullbool1" value="True" id="id_hidden_nullbool1" /><input type="hidden" name="hidden_nullbool2" value="False" id="id_hidden_nullbool2" />', str(f)) + + def test_nullbooleanfield_58(self): + class HiddenNullBooleanForm(Form): + hidden_nullbool1 = NullBooleanField(widget=HiddenInput, initial=True) + hidden_nullbool2 = NullBooleanField(widget=HiddenInput, initial=False) + f = HiddenNullBooleanForm({ 'hidden_nullbool1': 'True', 'hidden_nullbool2': 'False' }) + self.assertEqual(None, f.full_clean()) + self.assertEqual(True, f.cleaned_data['hidden_nullbool1']) + self.assertEqual(False, f.cleaned_data['hidden_nullbool2']) + + def test_nullbooleanfield_59(self): + # Make sure we're compatible with MySQL, which uses 0 and 1 for its boolean + # values. (#9609) + NULLBOOL_CHOICES = (('1', 'Yes'), ('0', 'No'), ('', 'Unknown')) + class MySQLNullBooleanForm(Form): + nullbool0 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) + nullbool1 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) + nullbool2 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) + f = MySQLNullBooleanForm({ 'nullbool0': '1', 'nullbool1': '0', 'nullbool2': '' }) + self.assertEqual(None, f.full_clean()) + self.assertEqual(True, f.cleaned_data['nullbool0']) + self.assertEqual(False, f.cleaned_data['nullbool1']) + self.assertEqual(None, f.cleaned_data['nullbool2']) + + # MultipleChoiceField ######################################################### + + def test_multiplechoicefield_60(self): + f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')]) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual([u'1'], f.clean([1])) + self.assertEqual([u'1'], f.clean(['1'])) + self.assertEqual([u'1', u'2'], f.clean(['1', '2'])) + self.assertEqual([u'1', u'2'], f.clean([1, '2'])) + self.assertEqual([u'1', u'2'], f.clean((1, '2'))) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, []) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, ()) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, ['3']) + + def test_multiplechoicefield_61(self): + f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) + self.assertEqual([], f.clean('')) + self.assertEqual([], f.clean(None)) + self.assertEqual([u'1'], f.clean([1])) + self.assertEqual([u'1'], f.clean(['1'])) + self.assertEqual([u'1', u'2'], f.clean(['1', '2'])) + self.assertEqual([u'1', u'2'], f.clean([1, '2'])) + self.assertEqual([u'1', u'2'], f.clean((1, '2'))) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertEqual([], f.clean([])) + self.assertEqual([], f.clean(())) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, ['3']) + + def test_multiplechoicefield_62(self): + f = MultipleChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) + self.assertEqual([u'1'], f.clean([1])) + self.assertEqual([u'1'], f.clean(['1'])) + self.assertEqual([u'1', u'5'], f.clean([1, 5])) + self.assertEqual([u'1', u'5'], f.clean([1, '5'])) + self.assertEqual([u'1', u'5'], f.clean(['1', 5])) + self.assertEqual([u'1', u'5'], f.clean(['1', '5'])) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['6']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['1','6']) + + # ComboField ################################################################## + + def test_combofield_63(self): + f = ComboField(fields=[CharField(max_length=20), EmailField()]) + self.assertEqual(u'test@example.com', f.clean('test@example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 20 characters (it has 28).']", f.clean, 'longemailaddress@example.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'not an e-mail') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + + def test_combofield_64(self): + f = ComboField(fields=[CharField(max_length=20), EmailField()], required=False) + self.assertEqual(u'test@example.com', f.clean('test@example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 20 characters (it has 28).']", f.clean, 'longemailaddress@example.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'not an e-mail') + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + + # FilePathField ############################################################### + + def test_filepathfield_65(self): + path = forms.__file__ + path = os.path.dirname(path) + '/' + assert fix_os_paths(path).endswith('/django/forms/') + + def test_filepathfield_66(self): + path = forms.__file__ + path = os.path.dirname(path) + '/' + f = FilePathField(path=path) + f.choices = [p for p in f.choices if p[0].endswith('.py')] + f.choices.sort() + expected = [ + ('/django/forms/__init__.py', '__init__.py'), + ('/django/forms/fields.py', 'fields.py'), + ('/django/forms/forms.py', 'forms.py'), + ('/django/forms/formsets.py', 'formsets.py'), + ('/django/forms/models.py', 'models.py'), + ('/django/forms/util.py', 'util.py'), + ('/django/forms/widgets.py', 'widgets.py') + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + assert got[0].endswith(exp[0]) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. fields.py is not one of the available choices.']", f.clean, 'fields.py') + assert fix_os_paths(f.clean(path + 'fields.py')).endswith('/django/forms/fields.py') + + def test_filepathfield_67(self): + path = forms.__file__ + path = os.path.dirname(path) + '/' + f = FilePathField(path=path, match='^.*?\.py$') + f.choices.sort() + expected = [ + ('/django/forms/__init__.py', '__init__.py'), + ('/django/forms/fields.py', 'fields.py'), + ('/django/forms/forms.py', 'forms.py'), + ('/django/forms/formsets.py', 'formsets.py'), + ('/django/forms/models.py', 'models.py'), + ('/django/forms/util.py', 'util.py'), + ('/django/forms/widgets.py', 'widgets.py') + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + assert got[0].endswith(exp[0]) + + def test_filepathfield_68(self): + path = forms.__file__ + path = os.path.dirname(path) + '/' + f = FilePathField(path=path, recursive=True, match='^.*?\.py$') + f.choices.sort() + expected = [ + ('/django/forms/__init__.py', '__init__.py'), + ('/django/forms/extras/__init__.py', 'extras/__init__.py'), + ('/django/forms/extras/widgets.py', 'extras/widgets.py'), + ('/django/forms/fields.py', 'fields.py'), + ('/django/forms/forms.py', 'forms.py'), + ('/django/forms/formsets.py', 'formsets.py'), + ('/django/forms/models.py', 'models.py'), + ('/django/forms/util.py', 'util.py'), + ('/django/forms/widgets.py', 'widgets.py') + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + assert got[0].endswith(exp[0]) + + # SplitDateTimeField ########################################################## + + def test_splitdatetimefield_69(self): + from django.forms.widgets import SplitDateTimeWidget + f = SplitDateTimeField() + assert isinstance(f.widget, SplitDateTimeWidget) + self.assertEqual(datetime.datetime(2006, 1, 10, 7, 30), f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.', u'Enter a valid time.']", f.clean, ['hello', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, ['hello', '07:30']) + + def test_splitdatetimefield_70(self): + f = SplitDateTimeField(required=False) + self.assertEqual(datetime.datetime(2006, 1, 10, 7, 30), f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])) + self.assertEqual(datetime.datetime(2006, 1, 10, 7, 30), f.clean(['2006-01-10', '07:30'])) + self.assertEqual(None, f.clean(None)) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean([''])) + self.assertEqual(None, f.clean(['', ''])) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.', u'Enter a valid time.']", f.clean, ['hello', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, ['hello', '07:30']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10', '']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, ['', '07:30']) diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index bf9623fe77..627f50a6fe 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1807,4 +1807,43 @@ True >>> [f.name for f in form.visible_fields()] ['artist', 'name'] +# Hidden initial input gets its own unique id ################################ + +>>> class MyForm(Form): +... field1 = CharField(max_length=50, show_hidden_initial=True) +>>> print MyForm() +<tr><th><label for="id_field1">Field1:</label></th><td><input id="id_field1" type="text" name="field1" maxlength="50" /><input type="hidden" name="initial-field1" id="initial-id_field1" /></td></tr> + +# The error_html_class and required_html_class attributes #################### + +>>> p = Person({}) +>>> p.error_css_class = 'error' +>>> p.required_css_class = 'required' + +>>> print p.as_ul() +<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></li> +<li class="required"><label for="id_is_cool">Is cool:</label> <select name="is_cool" id="id_is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select></li> + +>>> print p.as_p() +<ul class="errorlist"><li>This field is required.</li></ul> +<p class="required error"><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></p> +<p class="required"><label for="id_is_cool">Is cool:</label> <select name="is_cool" id="id_is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select></p> + +>>> print p.as_table() +<tr class="required error"><th><label for="id_name">Name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="name" id="id_name" /></td></tr> +<tr class="required"><th><label for="id_is_cool">Is cool:</label></th><td><select name="is_cool" id="id_is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select></td></tr> + + """ diff --git a/tests/regressiontests/forms/localflavor/ca.py b/tests/regressiontests/forms/localflavor/ca.py index 48171b0558..7f4b3ac89c 100644 --- a/tests/regressiontests/forms/localflavor/ca.py +++ b/tests/regressiontests/forms/localflavor/ca.py @@ -60,6 +60,50 @@ ValidationError: [u'Enter a postal code in the format XXX XXX.'] u'' >>> f.clean('') u'' +>>> f.clean('W2S 2H3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('T2W 2H7') +u'T2W 2H7' +>>> f.clean('T2S 2W7') +u'T2S 2W7' +>>> f.clean('Z2S 2H3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('T2Z 2H7') +u'T2Z 2H7' +>>> f.clean('T2S 2Z7') +u'T2S 2Z7' +>>> f.clean('F2S 2H3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('A2S 2D3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('A2I 2R3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('A2I 2R3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('A2Q 2R3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('U2B 2R3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('O2B 2R3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] # CAPhoneNumberField ########################################################## diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 6d418fa5a3..6f00a06fdd 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from extra import tests as extra_tests -from fields import tests as fields_tests from forms import tests as form_tests from error_messages import tests as custom_error_message_tests from localflavor.ar import tests as localflavor_ar_tests @@ -32,9 +31,10 @@ from widgets import tests as widgets_tests from formsets import tests as formset_tests from media import media_tests +from fields import FieldsTests + __test__ = { 'extra_tests': extra_tests, - 'fields_tests': fields_tests, 'form_tests': form_tests, 'custom_error_message_tests': custom_error_message_tests, 'localflavor_ar_tests': localflavor_ar_tests, diff --git a/tests/regressiontests/localflavor/__init__.py b/tests/regressiontests/localflavor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/localflavor/__init__.py diff --git a/tests/regressiontests/localflavor/forms.py b/tests/regressiontests/localflavor/forms.py new file mode 100644 index 0000000000..49635b02fb --- /dev/null +++ b/tests/regressiontests/localflavor/forms.py @@ -0,0 +1,14 @@ +from django.forms import ModelForm +from models import Place + +class PlaceForm(ModelForm): + """docstring for PlaceForm""" + class Meta: + model = Place +from django.forms import ModelForm +from models import Place + +class PlaceForm(ModelForm): + """docstring for PlaceForm""" + class Meta: + model = Place diff --git a/tests/regressiontests/localflavor/models.py b/tests/regressiontests/localflavor/models.py new file mode 100644 index 0000000000..079c7bd982 --- /dev/null +++ b/tests/regressiontests/localflavor/models.py @@ -0,0 +1,16 @@ +from django.db import models +from django.contrib.localflavor.us.models import USStateField + +class Place(models.Model): + state = USStateField(blank=True) + state_req = USStateField() + state_default = USStateField(default="CA", blank=True) + name = models.CharField(max_length=20) +from django.db import models +from django.contrib.localflavor.us.models import USStateField + +class Place(models.Model): + state = USStateField(blank=True) + state_req = USStateField() + state_default = USStateField(default="CA", blank=True) + name = models.CharField(max_length=20) diff --git a/tests/regressiontests/localflavor/tests.py b/tests/regressiontests/localflavor/tests.py new file mode 100644 index 0000000000..61e0e5b5e6 --- /dev/null +++ b/tests/regressiontests/localflavor/tests.py @@ -0,0 +1,166 @@ +from django.test import TestCase +from models import Place +from forms import PlaceForm + +class USLocalflavorTests(TestCase): + def setUp(self): + self.form = PlaceForm({'state':'GA', 'state_req':'NC', 'name':'impossible'}) + + def test_get_display_methods(self): + """Test that the get_*_display() methods are added to the model instances.""" + place = self.form.save() + self.assertEqual(place.get_state_display(), 'Georgia') + self.assertEqual(place.get_state_req_display(), 'North Carolina') + + def test_required(self): + """Test that required USStateFields throw appropriate errors.""" + form = PlaceForm({'state':'GA', 'name':'Place in GA'}) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['state_req'], [u'This field is required.']) + + def test_field_blank_option(self): + """Test that the empty option is there.""" + state_select_html = """\ +<select name="state" id="id_state"> +<option value="">---------</option> +<option value="AL">Alabama</option> +<option value="AK">Alaska</option> +<option value="AS">American Samoa</option> +<option value="AZ">Arizona</option> +<option value="AR">Arkansas</option> +<option value="CA">California</option> +<option value="CO">Colorado</option> +<option value="CT">Connecticut</option> +<option value="DE">Delaware</option> +<option value="DC">District of Columbia</option> +<option value="FL">Florida</option> +<option value="GA" selected="selected">Georgia</option> +<option value="GU">Guam</option> +<option value="HI">Hawaii</option> +<option value="ID">Idaho</option> +<option value="IL">Illinois</option> +<option value="IN">Indiana</option> +<option value="IA">Iowa</option> +<option value="KS">Kansas</option> +<option value="KY">Kentucky</option> +<option value="LA">Louisiana</option> +<option value="ME">Maine</option> +<option value="MD">Maryland</option> +<option value="MA">Massachusetts</option> +<option value="MI">Michigan</option> +<option value="MN">Minnesota</option> +<option value="MS">Mississippi</option> +<option value="MO">Missouri</option> +<option value="MT">Montana</option> +<option value="NE">Nebraska</option> +<option value="NV">Nevada</option> +<option value="NH">New Hampshire</option> +<option value="NJ">New Jersey</option> +<option value="NM">New Mexico</option> +<option value="NY">New York</option> +<option value="NC">North Carolina</option> +<option value="ND">North Dakota</option> +<option value="MP">Northern Mariana Islands</option> +<option value="OH">Ohio</option> +<option value="OK">Oklahoma</option> +<option value="OR">Oregon</option> +<option value="PA">Pennsylvania</option> +<option value="PR">Puerto Rico</option> +<option value="RI">Rhode Island</option> +<option value="SC">South Carolina</option> +<option value="SD">South Dakota</option> +<option value="TN">Tennessee</option> +<option value="TX">Texas</option> +<option value="UT">Utah</option> +<option value="VT">Vermont</option> +<option value="VI">Virgin Islands</option> +<option value="VA">Virginia</option> +<option value="WA">Washington</option> +<option value="WV">West Virginia</option> +<option value="WI">Wisconsin</option> +<option value="WY">Wyoming</option> +</select>""" + self.assertEqual(str(self.form['state']), state_select_html) +from django.test import TestCase +from models import Place +from forms import PlaceForm + +class USLocalflavorTests(TestCase): + def setUp(self): + self.form = PlaceForm({'state':'GA', 'state_req':'NC', 'name':'impossible'}) + + def test_get_display_methods(self): + """Test that the get_*_display() methods are added to the model instances.""" + place = self.form.save() + self.assertEqual(place.get_state_display(), 'Georgia') + self.assertEqual(place.get_state_req_display(), 'North Carolina') + + def test_required(self): + """Test that required USStateFields throw appropriate errors.""" + form = PlaceForm({'state':'GA', 'name':'Place in GA'}) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['state_req'], [u'This field is required.']) + + def test_field_blank_option(self): + """Test that the empty option is there.""" + state_select_html = """\ +<select name="state" id="id_state"> +<option value="">---------</option> +<option value="AL">Alabama</option> +<option value="AK">Alaska</option> +<option value="AS">American Samoa</option> +<option value="AZ">Arizona</option> +<option value="AR">Arkansas</option> +<option value="CA">California</option> +<option value="CO">Colorado</option> +<option value="CT">Connecticut</option> +<option value="DE">Delaware</option> +<option value="DC">District of Columbia</option> +<option value="FL">Florida</option> +<option value="GA" selected="selected">Georgia</option> +<option value="GU">Guam</option> +<option value="HI">Hawaii</option> +<option value="ID">Idaho</option> +<option value="IL">Illinois</option> +<option value="IN">Indiana</option> +<option value="IA">Iowa</option> +<option value="KS">Kansas</option> +<option value="KY">Kentucky</option> +<option value="LA">Louisiana</option> +<option value="ME">Maine</option> +<option value="MD">Maryland</option> +<option value="MA">Massachusetts</option> +<option value="MI">Michigan</option> +<option value="MN">Minnesota</option> +<option value="MS">Mississippi</option> +<option value="MO">Missouri</option> +<option value="MT">Montana</option> +<option value="NE">Nebraska</option> +<option value="NV">Nevada</option> +<option value="NH">New Hampshire</option> +<option value="NJ">New Jersey</option> +<option value="NM">New Mexico</option> +<option value="NY">New York</option> +<option value="NC">North Carolina</option> +<option value="ND">North Dakota</option> +<option value="MP">Northern Mariana Islands</option> +<option value="OH">Ohio</option> +<option value="OK">Oklahoma</option> +<option value="OR">Oregon</option> +<option value="PA">Pennsylvania</option> +<option value="PR">Puerto Rico</option> +<option value="RI">Rhode Island</option> +<option value="SC">South Carolina</option> +<option value="SD">South Dakota</option> +<option value="TN">Tennessee</option> +<option value="TX">Texas</option> +<option value="UT">Utah</option> +<option value="VT">Vermont</option> +<option value="VI">Virgin Islands</option> +<option value="VA">Virginia</option> +<option value="WA">Washington</option> +<option value="WV">West Virginia</option> +<option value="WI">Wisconsin</option> +<option value="WY">Wyoming</option> +</select>""" + self.assertEqual(str(self.form['state']), state_select_html) diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py index 85e284b639..f8b6511140 100644 --- a/tests/regressiontests/model_forms_regress/tests.py +++ b/tests/regressiontests/model_forms_regress/tests.py @@ -100,4 +100,16 @@ class CustomFieldSaveTests(TestCase): # It's enough that the form saves without error -- the custom save routine will # generate an AssertionError if it is called more than once during save. form = CFFForm(data = {'f': None}) - form.save()
\ No newline at end of file + form.save() + +class ModelChoiceIteratorTests(TestCase): + def test_len(self): + class Form(forms.ModelForm): + class Meta: + model = Article + fields = ["publications"] + + Publication.objects.create(title="Pravda", + date_published=date(1991, 8, 22)) + f = Form() + self.assertEqual(len(f.fields["publications"].choices), 1) diff --git a/tests/regressiontests/settings_tests/__init__.py b/tests/regressiontests/settings_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/settings_tests/__init__.py diff --git a/tests/regressiontests/settings_tests/models.py b/tests/regressiontests/settings_tests/models.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/settings_tests/models.py diff --git a/tests/regressiontests/settings_tests/tests.py b/tests/regressiontests/settings_tests/tests.py new file mode 100644 index 0000000000..fa217b1d79 --- /dev/null +++ b/tests/regressiontests/settings_tests/tests.py @@ -0,0 +1,17 @@ +import unittest +from django.conf import settings + +class SettingsTests(unittest.TestCase): + + # + # Regression tests for #10130: deleting settings. + # + + def test_settings_delete(self): + settings.TEST = 'test' + self.assertEqual('test', settings.TEST) + del settings.TEST + self.assertRaises(AttributeError, getattr, settings, 'TEST') + + def test_settings_delete_wrapped(self): + self.assertRaises(TypeError, delattr, settings, '_wrapped') diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py index 2b448574f7..88266e107b 100644 --- a/tests/regressiontests/templates/filters.py +++ b/tests/regressiontests/templates/filters.py @@ -120,13 +120,19 @@ def get_filter_tests(): # Notice that escaping is applied *after* any filters, so the string # formatting here only needs to deal with pre-escaped characters. - 'filter-stringformat01': ('{% autoescape off %}.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.{% endautoescape %}', {"a": "a<b", "b": mark_safe("a<b")}, u". a<b. . a<b."), - 'filter-stringformat02': ('.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.', {"a": "a<b", "b": mark_safe("a<b")}, u". a<b. . a<b."), - - # XXX No test for "title" filter; needs an actual object. - - 'filter-truncatewords01': ('{% autoescape off %}{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}{% endautoescape %}', {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), - 'filter-truncatewords02': ('{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}', {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), + 'filter-stringformat01': ('{% autoescape off %}.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.{% endautoescape %}', + {"a": "a<b", "b": mark_safe("a<b")}, u". a<b. . a<b."), + 'filter-stringformat02': ('.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.', {"a": "a<b", "b": mark_safe("a<b")}, + u". a<b. . a<b."), + + # Test the title filter + 'filter-title1' : ('{{ a|title }}', {'a' : 'JOE\'S CRAB SHACK'}, u'Joe's Crab Shack'), + 'filter-title2' : ('{{ a|title }}', {'a' : '555 WEST 53RD STREET'}, u'555 West 53rd Street'), + + 'filter-truncatewords01': ('{% autoescape off %}{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}{% endautoescape %}', + {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), + 'filter-truncatewords02': ('{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}', + {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), # The "upper" filter messes up entities (which are case-sensitive), # so it's not safe for non-escaping purposes. diff --git a/tests/regressiontests/test_runner/__init__.py b/tests/regressiontests/test_runner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/test_runner/__init__.py diff --git a/tests/regressiontests/test_runner/models.py b/tests/regressiontests/test_runner/models.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/test_runner/models.py diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py new file mode 100644 index 0000000000..af8a29328a --- /dev/null +++ b/tests/regressiontests/test_runner/tests.py @@ -0,0 +1,29 @@ +""" +Tests for django test runner +""" +import StringIO +import unittest +import django +from django.test import TestCase, TransactionTestCase, simple + +class DjangoTestRunnerTests(TestCase): + + def test_failfast(self): + class MockTestOne(TransactionTestCase): + def runTest(self): + assert False + class MockTestTwo(TransactionTestCase): + def runTest(self): + assert False + + suite = unittest.TestSuite([MockTestOne(), MockTestTwo()]) + mock_stream = StringIO.StringIO() + dtr = simple.DjangoTestRunner(verbosity=0, failfast=False, stream=mock_stream) + result = dtr.run(suite) + self.assertEqual(2, result.testsRun) + self.assertEqual(2, len(result.failures)) + + dtr = simple.DjangoTestRunner(verbosity=0, failfast=True, stream=mock_stream) + result = dtr.run(suite) + self.assertEqual(1, result.testsRun) + self.assertEqual(1, len(result.failures)) diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 34c9db25d3..1dc85b0424 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -244,7 +244,6 @@ class NamespaceTests(TestCase): self.assertEquals('/other1/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42], current_app='other-ns1')) self.assertEquals('/other1/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='other-ns1')) - class RequestURLconfTests(TestCase): def setUp(self): self.root_urlconf = settings.ROOT_URLCONF @@ -276,3 +275,25 @@ class RequestURLconfTests(TestCase): response = self.client.get('/second_test/') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'outer:,inner:/second_test/') + +class ErrorHandlerResolutionTests(TestCase): + """Tests for handler404 and handler500""" + + def setUp(self): + from django.core.urlresolvers import RegexURLResolver + urlconf = 'regressiontests.urlpatterns_reverse.urls_error_handlers' + urlconf_callables = 'regressiontests.urlpatterns_reverse.urls_error_handlers_callables' + self.resolver = RegexURLResolver(r'^$', urlconf) + self.callable_resolver = RegexURLResolver(r'^$', urlconf_callables) + + def test_named_handlers(self): + from views import empty_view + handler = (empty_view, {}) + self.assertEqual(self.resolver.resolve404(), handler) + self.assertEqual(self.resolver.resolve500(), handler) + + def test_callable_handers(self): + from views import empty_view + handler = (empty_view, {}) + self.assertEqual(self.callable_resolver.resolve404(), handler) + self.assertEqual(self.callable_resolver.resolve500(), handler) diff --git a/tests/regressiontests/urlpatterns_reverse/urls_error_handlers.py b/tests/regressiontests/urlpatterns_reverse/urls_error_handlers.py new file mode 100644 index 0000000000..c2e0d32498 --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/urls_error_handlers.py @@ -0,0 +1,8 @@ +# Used by the ErrorHandlerResolutionTests test case. + +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('') + +handler404 = 'regressiontests.urlpatterns_reverse.views.empty_view' +handler500 = 'regressiontests.urlpatterns_reverse.views.empty_view' diff --git a/tests/regressiontests/urlpatterns_reverse/urls_error_handlers_callables.py b/tests/regressiontests/urlpatterns_reverse/urls_error_handlers_callables.py new file mode 100644 index 0000000000..00f25a7236 --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/urls_error_handlers_callables.py @@ -0,0 +1,9 @@ +# Used by the ErrorHandlerResolutionTests test case. + +from django.conf.urls.defaults import patterns +from views import empty_view + +urlpatterns = patterns('') + +handler404 = empty_view +handler500 = empty_view diff --git a/tests/runtests.py b/tests/runtests.py index 86e83fe1ea..1cd95b9fef 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -86,7 +86,7 @@ class InvalidModelTestCase(unittest.TestCase): self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected)) self.assert_(not missing, "Missing Errors: " + '\n'.join(missing)) -def django_tests(verbosity, interactive, test_labels): +def django_tests(verbosity, interactive, failfast, test_labels): from django.conf import settings old_installed_apps = settings.INSTALLED_APPS @@ -159,7 +159,8 @@ def django_tests(verbosity, interactive, test_labels): settings.TEST_RUNNER = 'django.test.simple.run_tests' test_runner = get_runner(settings) - failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive, extra_tests=extra_tests) + failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive, failfast=failfast, + extra_tests=extra_tests) if failures: sys.exit(failures) @@ -181,6 +182,8 @@ if __name__ == "__main__": help='Verbosity level; 0=minimal output, 1=normal output, 2=all output') parser.add_option('--noinput', action='store_false', dest='interactive', default=True, help='Tells Django to NOT prompt the user for input of any kind.') + parser.add_option('--failfast', action='store_true', dest='failfast', default=False, + help='Tells Django to stop running the test suite after first failed test.') parser.add_option('--settings', help='Python path to settings module, e.g. "myproject.settings". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.') options, args = parser.parse_args() @@ -189,4 +192,4 @@ if __name__ == "__main__": elif "DJANGO_SETTINGS_MODULE" not in os.environ: parser.error("DJANGO_SETTINGS_MODULE is not set in the environment. " "Set it or use --settings.") - django_tests(int(options.verbosity), options.interactive, args) + django_tests(int(options.verbosity), options.interactive, options.failfast, args) |
