diff --git a/AUTHORS b/AUTHORS index 27fe91bcc5..585b1c8db6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -55,6 +55,7 @@ answer newbie questions, and generally made Django that much better: Niran Babalola Morten Bagai Mikaël Barbero + Randy Barlow Scott Barr Jiri Barton Ned Batchelder @@ -136,7 +137,7 @@ answer newbie questions, and generally made Django that much better: Andrew Durdin dusk@woofle.net Andy Dustman - J. Clifford Dyer + J. Clifford Dyer Clint Ecker Nick Efford eibaan@gmail.com @@ -172,6 +173,7 @@ answer newbie questions, and generally made Django that much better: Alex Gaynor Andy Gayton Idan Gazit + geber@datacollect.com Baishampayan Ghose Dimitris Glezos glin@seznam.cz @@ -267,6 +269,7 @@ answer newbie questions, and generally made Django that much better: Finn Gruwier Larsen Lau Bech Lauritzen Rune Rønde Laursen + Mark Lavin Eugene Lazutkin lcordier@point45.com Jeong-Min Lee @@ -300,6 +303,7 @@ answer newbie questions, and generally made Django that much better: Jason McBrayer Kevin McConnell mccutchen@gmail.com + Paul McLanahan Tobias McNulty Christian Metts michael.mcewan@gmail.com @@ -382,6 +386,7 @@ answer newbie questions, and generally made Django that much better: Massimo Scamarcia David Schein Bernd Schlapsi + schwank@gmail.com scott@staplefish.com Ilya Semenov serbaut@gmail.com @@ -393,6 +398,7 @@ answer newbie questions, and generally made Django that much better: Jozko Skrablin Ben Slavin sloonz + Paul Smith Warren Smith smurf@smurf.noris.de Vsevolod Solovyov diff --git a/README b/README index 720b6c76df..f4476be925 100644 --- a/README +++ b/README @@ -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 s -- excluding the
." - return self._html_output(u'%(label)s%(errors)s%(field)s%(help_text)s', u'%s', '', u'
%s', False) + return self._html_output( + normal_row = u'%(label)s%(errors)s%(field)s%(help_text)s', + error_row = u'%s', + row_ender = u'', + help_text_html = u'
%s', + errors_on_separate_row = False) def as_ul(self): "Returns this form rendered as HTML
  • s -- excluding the
      ." - return self._html_output(u'
    • %(errors)s%(label)s %(field)s%(help_text)s
    • ', u'
    • %s
    • ', '', u' %s', False) + return self._html_output( + normal_row = u'%(errors)s%(label)s %(field)s%(help_text)s', + error_row = u'
    • %s
    • ', + row_ender = '', + help_text_html = u' %s', + errors_on_separate_row = False) def as_p(self): "Returns this form rendered as HTML

      s." - return self._html_output(u'

      %(label)s %(field)s%(help_text)s

      ', u'%s', '

      ', u' %s', True) + return self._html_output( + normal_row = u'%(label)s %(field)s%(help_text)s

      ', + error_row = u'%s', + row_ender = '

      ', + 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'' % (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 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 -------------------------------- 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:: +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() + ... + ... + ... +