From e29358827d1a384bac2f4590e113fc681796080a Mon Sep 17 00:00:00 2001 From: Joseph Kocherhans Date: Tue, 6 Nov 2007 22:25:59 +0000 Subject: [PATCH] newforms-admin: Merged to [6652] git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@6656 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/locale/es/LC_MESSAGES/django.mo | Bin 61344 -> 61344 bytes django/conf/locale/es/LC_MESSAGES/django.po | 2 +- django/contrib/auth/forms.py | 2 +- django/contrib/sessions/middleware.py | 20 +- django/core/management/__init__.py | 2 + django/core/management/commands/startapp.py | 21 +- django/core/management/sql.py | 63 +- django/core/servers/basehttp.py | 6 +- django/db/backends/__init__.py | 1 + django/db/backends/mysql/base.py | 1 + django/db/backends/mysql_old/base.py | 1 + django/db/models/__init__.py | 1 + django/db/models/fields/__init__.py | 2 + django/db/models/fields/subclassing.py | 53 ++ django/middleware/http.py | 5 +- django/newforms/fields.py | 230 +++++-- django/newforms/models.py | 70 ++- django/newforms/util.py | 10 +- django/template/defaultfilters.py | 187 +++--- django/template/defaulttags.py | 197 +++--- django/test/testcases.py | 2 +- django/utils/cache.py | 30 +- django/utils/dateformat.py | 2 +- django/utils/encoding.py | 28 +- django/utils/http.py | 28 + django/views/static.py | 8 +- docs/cache.txt | 10 +- docs/custom_model_fields.txt | 567 ++++++++++++++++++ docs/email.txt | 2 +- docs/form_preview.txt | 2 +- docs/model-api.txt | 109 +--- docs/modpython.txt | 2 +- docs/newforms.txt | 51 +- docs/serialization.txt | 27 +- docs/templates.txt | 44 +- docs/testing.txt | 1 - .../modeltests/field_subclassing/__init__.py | 0 tests/modeltests/field_subclassing/models.py | 106 ++++ tests/regressiontests/defaultfilters/tests.py | 4 + tests/regressiontests/forms/error_messages.py | 315 ++++++++++ tests/regressiontests/forms/extra.py | 3 +- tests/regressiontests/forms/regressions.py | 1 - tests/regressiontests/forms/tests.py | 2 + tests/regressiontests/forms/util.py | 7 + tests/regressiontests/templates/urls.py | 2 +- tests/regressiontests/text/tests.py | 8 + 46 files changed, 1768 insertions(+), 467 deletions(-) create mode 100644 django/db/models/fields/subclassing.py create mode 100644 docs/custom_model_fields.txt create mode 100644 tests/modeltests/field_subclassing/__init__.py create mode 100644 tests/modeltests/field_subclassing/models.py create mode 100644 tests/regressiontests/forms/error_messages.py diff --git a/django/conf/locale/es/LC_MESSAGES/django.mo b/django/conf/locale/es/LC_MESSAGES/django.mo index 29b411467e4bb30a442b596b9129e233f487e92c..728c53a15bad6a3e60fd75780780ae902fec51dc 100644 GIT binary patch delta 17 ZcmZ2*pLxN3<_+A7nTql^^Ddt30{};q2Z#Uw delta 17 ZcmZ2*pLxN3<_+A7nevM^^Ddt30{};n2Z#Uw diff --git a/django/conf/locale/es/LC_MESSAGES/django.po b/django/conf/locale/es/LC_MESSAGES/django.po index 786889106e..509e01f7c0 100644 --- a/django/conf/locale/es/LC_MESSAGES/django.po +++ b/django/conf/locale/es/LC_MESSAGES/django.po @@ -1016,7 +1016,7 @@ msgstr "Escoja %s para modificar" #: contrib/admin/views/main.py:780 msgid "Database error" -msgstr "Erorr en la base de datos" +msgstr "Error en la base de datos" #: contrib/auth/forms.py:17 #: contrib/auth/forms.py:138 diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 16ee0289a2..47a974cacd 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -104,7 +104,7 @@ class PasswordResetForm(oldforms.Manipulator): 'site_name': site_name, 'user': user, } - send_mail('Password reset on %s' % site_name, t.render(Context(c)), None, [user.email]) + send_mail(_('Password reset on %s') % site_name, t.render(Context(c)), None, [user.email]) class PasswordChangeForm(oldforms.Manipulator): "A form that lets a user change his password." diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py index 7b6c826805..2af2312e76 100644 --- a/django/contrib/sessions/middleware.py +++ b/django/contrib/sessions/middleware.py @@ -1,8 +1,8 @@ +import time + from django.conf import settings from django.utils.cache import patch_vary_headers -from email.Utils import formatdate -import datetime -import time +from django.utils.http import cookie_date TEST_COOKIE_NAME = 'testcookie' TEST_COOKIE_VALUE = 'worked' @@ -10,8 +10,9 @@ TEST_COOKIE_VALUE = 'worked' class SessionMiddleware(object): def process_request(self, request): - engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) - request.session = engine.SessionStore(request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)) + engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) + session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) + request.session = engine.SessionStore(session_key) def process_response(self, request, response): # If request.session was modified, or if response.session was set, save @@ -30,13 +31,8 @@ class SessionMiddleware(object): expires = None else: max_age = settings.SESSION_COOKIE_AGE - rfcdate = formatdate(time.time() + settings.SESSION_COOKIE_AGE) - - # Fixed length date must have '-' separation in the format - # DD-MMM-YYYY for compliance with Netscape cookie standard - expires = datetime.datetime.strftime(datetime.datetime.utcnow() + \ - datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE), "%a, %d-%b-%Y %H:%M:%S GMT") - + expires_time = time.time() + settings.SESSION_COOKIE_AGE + expires = cookie_date(expires_time) # Save the seesion data and refresh the client cookie. request.session.save() response.set_cookie(settings.SESSION_COOKIE_NAME, diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index f706fa3c7e..dce2fd493d 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -242,6 +242,8 @@ def setup_environ(settings_mod): """ Configures the runtime environment. This can also be used by external scripts wanting to set up a similar environment to manage.py. + Returns the project directory (assuming the passed settings module is + directly in the project directory). """ # Add this project to sys.path so that it's importable in the conventional # way. For example, if this file (manage.py) lives in a directory diff --git a/django/core/management/commands/startapp.py b/django/core/management/commands/startapp.py index c238e74a08..a3d517dd7a 100644 --- a/django/core/management/commands/startapp.py +++ b/django/core/management/commands/startapp.py @@ -1,8 +1,10 @@ -from django.core.management.base import copy_helper, CommandError, LabelCommand import os +from django.core.management.base import copy_helper, CommandError, LabelCommand + class Command(LabelCommand): - help = "Creates a Django app directory structure for the given app name in the current directory." + help = ("Creates a Django app directory structure for the given app name" + " in the current directory.") args = "[appname]" label = 'application name' @@ -14,17 +16,18 @@ class Command(LabelCommand): def handle_label(self, app_name, directory=None, **options): if directory is None: directory = os.getcwd() - # Determine the project_name a bit naively -- by looking at the name of - # the parent directory. - project_dir = os.path.normpath(os.path.join(directory, os.pardir)) - parent_dir = os.path.basename(project_dir) + # Determine the project_name by using the basename of directory, + # which should be the full path of the project directory (or the + # current directory if no directory was passed). project_name = os.path.basename(directory) if app_name == project_name: - raise CommandError("You cannot create an app with the same name (%r) as your project." % app_name) - copy_helper(self.style, 'app', app_name, directory, parent_dir) + raise CommandError("You cannot create an app with the same name" + " (%r) as your project." % app_name) + copy_helper(self.style, 'app', app_name, directory, project_name) class ProjectCommand(Command): - help = "Creates a Django app directory structure for the given app name in this project's directory." + help = ("Creates a Django app directory structure for the given app name" + " in this project's directory.") def __init__(self, project_directory): super(ProjectCommand, self).__init__() diff --git a/django/core/management/sql.py b/django/core/management/sql.py index d7a46f0cd6..43918ef1c2 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -252,6 +252,7 @@ def sql_model_create(model, style, known_models=set()): table_output = [] pending_references = {} qn = connection.ops.quote_name + inline_references = connection.features.inline_fk_references for f in opts.fields: col_type = f.db_type() tablespace = f.db_tablespace or opts.db_tablespace @@ -272,7 +273,7 @@ def sql_model_create(model, style, known_models=set()): # won't be generating a CREATE INDEX statement for this field. field_output.append(connection.ops.tablespace_sql(tablespace, inline=True)) if f.rel: - if f.rel.to in known_models: + if inline_references and f.rel.to in known_models: field_output.append(style.SQL_KEYWORD('REFERENCES') + ' ' + \ style.SQL_TABLE(qn(f.rel.to._meta.db_table)) + ' (' + \ style.SQL_FIELD(qn(f.rel.to._meta.get_field(f.rel.field_name).column)) + ')' + @@ -341,10 +342,12 @@ def sql_for_pending_references(model, style, pending_references): def many_to_many_sql_for_model(model, style): from django.db import connection, models from django.contrib.contenttypes import generic + from django.db.backends.util import truncate_name opts = model._meta final_output = [] qn = connection.ops.quote_name + inline_references = connection.features.inline_fk_references for f in opts.many_to_many: if not isinstance(f.rel, generic.GenericRel): tablespace = f.db_tablespace or opts.db_tablespace @@ -354,26 +357,43 @@ def many_to_many_sql_for_model(model, style): tablespace_sql = '' table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \ style.SQL_TABLE(qn(f.m2m_db_table())) + ' ('] - table_output.append(' %s %s %s%s,' % \ + table_output.append(' %s %s %s%s,' % (style.SQL_FIELD(qn('id')), style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type()), style.SQL_KEYWORD('NOT NULL PRIMARY KEY'), tablespace_sql)) - table_output.append(' %s %s %s %s (%s)%s,' % \ - (style.SQL_FIELD(qn(f.m2m_column_name())), - style.SQL_COLTYPE(models.ForeignKey(model).db_type()), - style.SQL_KEYWORD('NOT NULL REFERENCES'), - style.SQL_TABLE(qn(opts.db_table)), - style.SQL_FIELD(qn(opts.pk.column)), - connection.ops.deferrable_sql())) - table_output.append(' %s %s %s %s (%s)%s,' % \ - (style.SQL_FIELD(qn(f.m2m_reverse_name())), - style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()), - style.SQL_KEYWORD('NOT NULL REFERENCES'), - style.SQL_TABLE(qn(f.rel.to._meta.db_table)), - style.SQL_FIELD(qn(f.rel.to._meta.pk.column)), - connection.ops.deferrable_sql())) - table_output.append(' %s (%s, %s)%s' % \ + if inline_references: + deferred = [] + table_output.append(' %s %s %s %s (%s)%s,' % + (style.SQL_FIELD(qn(f.m2m_column_name())), + style.SQL_COLTYPE(models.ForeignKey(model).db_type()), + style.SQL_KEYWORD('NOT NULL REFERENCES'), + style.SQL_TABLE(qn(opts.db_table)), + style.SQL_FIELD(qn(opts.pk.column)), + connection.ops.deferrable_sql())) + table_output.append(' %s %s %s %s (%s)%s,' % + (style.SQL_FIELD(qn(f.m2m_reverse_name())), + style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()), + style.SQL_KEYWORD('NOT NULL REFERENCES'), + style.SQL_TABLE(qn(f.rel.to._meta.db_table)), + style.SQL_FIELD(qn(f.rel.to._meta.pk.column)), + connection.ops.deferrable_sql())) + else: + table_output.append(' %s %s %s,' % + (style.SQL_FIELD(qn(f.m2m_column_name())), + style.SQL_COLTYPE(models.ForeignKey(model).db_type()), + style.SQL_KEYWORD('NOT NULL'))) + table_output.append(' %s %s %s,' % + (style.SQL_FIELD(qn(f.m2m_reverse_name())), + style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()), + style.SQL_KEYWORD('NOT NULL'))) + deferred = [ + (f.m2m_db_table(), f.m2m_column_name(), opts.db_table, + opts.pk.column), + ( f.m2m_db_table(), f.m2m_reverse_name(), + f.rel.to._meta.db_table, f.rel.to._meta.pk.column) + ] + table_output.append(' %s (%s, %s)%s' % (style.SQL_KEYWORD('UNIQUE'), style.SQL_FIELD(qn(f.m2m_column_name())), style.SQL_FIELD(qn(f.m2m_reverse_name())), @@ -385,6 +405,15 @@ def many_to_many_sql_for_model(model, style): table_output.append(';') final_output.append('\n'.join(table_output)) + for r_table, r_col, table, col in deferred: + r_name = '%s_refs_%s_%x' % (r_col, col, + abs(hash((r_table, table)))) + final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % + (qn(r_table), + truncate_name(r_name, connection.ops.max_name_length()), + qn(r_col), qn(table), qn(col), + connection.ops.deferrable_sql())) + # Add any extra SQL needed to support auto-incrementing PKs autoinc_sql = connection.ops.autoinc_sql(f.m2m_db_table(), 'id') if autoinc_sql: diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 411068b2f5..380e571b00 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -9,14 +9,14 @@ been reviewed for security issues. Don't use it for production use. from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from types import ListType, StringType -from email.Utils import formatdate import mimetypes import os import re import sys -import time import urllib +from django.utils.http import http_date + __version__ = "0.1" __all__ = ['WSGIServer','WSGIRequestHandler','demo_app'] @@ -376,7 +376,7 @@ class ServerHandler(object): self._write('HTTP/%s %s\r\n' % (self.http_version,self.status)) if 'Date' not in self.headers: self._write( - 'Date: %s\r\n' % (formatdate()[:26] + "GMT") + 'Date: %s\r\n' % http_date() ) if self.server_software and 'Server' not in self.headers: self._write('Server: %s\r\n' % self.server_software) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index dd50229461..be1776e65f 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -43,6 +43,7 @@ class BaseDatabaseFeatures(object): allows_group_by_ordinal = True allows_unique_and_pk = True autoindexes_primary_keys = True + inline_fk_references = True needs_datetime_string_cast = True needs_upper_for_iops = False supports_constraints = True diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index f729629751..ba3e9efda8 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -61,6 +61,7 @@ server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})') class DatabaseFeatures(BaseDatabaseFeatures): autoindexes_primary_keys = False + inline_fk_references = False class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): diff --git a/django/db/backends/mysql_old/base.py b/django/db/backends/mysql_old/base.py index 0777c1dc93..c22094b968 100644 --- a/django/db/backends/mysql_old/base.py +++ b/django/db/backends/mysql_old/base.py @@ -65,6 +65,7 @@ class MysqlDebugWrapper: class DatabaseFeatures(BaseDatabaseFeatures): autoindexes_primary_keys = False + inline_fk_references = False class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 9556459d44..bd6cc3542d 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -7,6 +7,7 @@ from django.db.models.query import Q from django.db.models.manager import Manager from django.db.models.base import Model from django.db.models.fields import * +from django.db.models.fields.subclassing import SubfieldBase from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED from django.db.models import signals from django.utils.functional import curry diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index a3d22d2770..be5e0ca45a 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -145,6 +145,8 @@ class Field(object): # exactly which wacky database column type you want to use. data_types = get_creation_module().DATA_TYPES internal_type = self.get_internal_type() + if internal_type not in data_types: + return None return data_types[internal_type] % self.__dict__ def validate_full(self, field_data, all_data): diff --git a/django/db/models/fields/subclassing.py b/django/db/models/fields/subclassing.py new file mode 100644 index 0000000000..1e4c8ca2e0 --- /dev/null +++ b/django/db/models/fields/subclassing.py @@ -0,0 +1,53 @@ +""" +Convenience routines for creating non-trivial Field subclasses. + +Add SubfieldBase as the __metaclass__ for your Field subclass, implement +to_python() and the other necessary methods and everything will work seamlessly. +""" + +from django.utils.maxlength import LegacyMaxlength + +class SubfieldBase(LegacyMaxlength): + """ + A metaclass for custom Field subclasses. This ensures the model's attribute + has the descriptor protocol attached to it. + """ + def __new__(cls, base, name, attrs): + new_class = super(SubfieldBase, cls).__new__(cls, base, name, attrs) + new_class.contribute_to_class = make_contrib( + attrs.get('contribute_to_class')) + return new_class + +class Creator(object): + """ + A placeholder class that provides a way to set the attribute on the model. + """ + def __init__(self, field): + self.field = field + + def __get__(self, obj, type=None): + if obj is None: + raise AttributeError('Can only be accessed via an instance.') + return self.value + + def __set__(self, obj, value): + self.value = self.field.to_python(value) + +def make_contrib(func=None): + """ + Returns a suitable contribute_to_class() method for the Field subclass. + + If 'func' is passed in, it is the existing contribute_to_class() method on + the subclass and it is called before anything else. It is assumed in this + case that the existing contribute_to_class() calls all the necessary + superclass methods. + """ + def contribute_to_class(self, cls, name): + if func: + func(self, cls, name) + else: + super(self.__class__, self).contribute_to_class(cls, name) + setattr(cls, self.name, Creator(self)) + + return contribute_to_class + diff --git a/django/middleware/http.py b/django/middleware/http.py index 71cdf7aa5d..d080ebcf0f 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -1,4 +1,4 @@ -from email.Utils import formatdate +from django.utils.http import http_date class ConditionalGetMiddleware(object): """ @@ -11,7 +11,7 @@ class ConditionalGetMiddleware(object): Also sets the Date and Content-Length response-headers. """ def process_response(self, request, response): - response['Date'] = formatdate()[:26] + "GMT" + response['Date'] = http_date() if not response.has_header('Content-Length'): response['Content-Length'] = str(len(response.content)) @@ -23,7 +23,6 @@ class ConditionalGetMiddleware(object): response['Content-Length'] = '0' if response.has_header('Last-Modified'): - last_mod = response['Last-Modified'] if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) if if_modified_since == response['Last-Modified']: response.status_code = 304 diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 11e6f51779..9bb2ced583 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -1,30 +1,35 @@ """ -Field classes +Field classes. """ import copy import datetime import re import time +# Python 2.3 fallbacks +try: + from decimal import Decimal, DecimalException +except ImportError: + from django.utils._decimal import Decimal, DecimalException +try: + set +except NameError: + from sets import Set as set -from django.utils.translation import ugettext +from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import StrAndUnicode, smart_unicode from util import ErrorList, ValidationError from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput -try: - from decimal import Decimal, DecimalException -except ImportError: - from django.utils._decimal import Decimal, DecimalException __all__ = ( 'Field', 'CharField', 'IntegerField', 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', - 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 'BooleanField', - 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', + 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', + 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 'SplitDateTimeField', 'IPAddressField', ) @@ -32,24 +37,20 @@ __all__ = ( # These values, if given to to_python(), will trigger the self.required check. EMPTY_VALUES = (None, '') -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback - -try: - from decimal import Decimal -except ImportError: - from django.utils._decimal import Decimal # Python 2.3 fallback class Field(object): widget = TextInput # Default widget to use when rendering this type of Field. hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". + default_error_messages = { + 'required': _(u'This field is required.'), + 'invalid': _(u'Enter a valid value.'), + } # Tracks each time a Field instance is created. Used to retain order. creation_counter = 0 - def __init__(self, required=True, widget=None, label=None, initial=None, help_text=None): + def __init__(self, required=True, widget=None, label=None, initial=None, + help_text=None, error_messages=None): # required -- Boolean that specifies whether the field is required. # True by default. # widget -- A Widget class, or instance of a Widget class, that should @@ -82,6 +83,22 @@ class Field(object): self.creation_counter = Field.creation_counter Field.creation_counter += 1 + self.error_messages = self._build_error_messages(error_messages) + + def _build_error_messages(self, extra_error_messages): + error_messages = {} + + def get_default_error_messages(klass): + for base_class in klass.__bases__: + get_default_error_messages(base_class) + if hasattr(klass, 'default_error_messages'): + error_messages.update(klass.default_error_messages) + + get_default_error_messages(self.__class__) + if extra_error_messages: + error_messages.update(extra_error_messages) + return error_messages + def clean(self, value): """ Validates the given value and returns its "cleaned" value as an @@ -90,7 +107,7 @@ class Field(object): Raises ValidationError for any errors. """ if self.required and value in EMPTY_VALUES: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) return value def widget_attrs(self, widget): @@ -108,6 +125,11 @@ class Field(object): return result class CharField(Field): + default_error_messages = { + 'max_length': _(u'Ensure this value has at most %(max)d characters (it has %(length)d).'), + 'min_length': _(u'Ensure this value has at least %(min)d characters (it has %(length)d).'), + } + def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length super(CharField, self).__init__(*args, **kwargs) @@ -120,9 +142,9 @@ class CharField(Field): value = smart_unicode(value) value_length = len(value) if self.max_length is not None and value_length > self.max_length: - raise ValidationError(ugettext(u'Ensure this value has at most %(max)d characters (it has %(length)d).') % {'max': self.max_length, 'length': value_length}) + raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length}) if self.min_length is not None and value_length < self.min_length: - raise ValidationError(ugettext(u'Ensure this value has at least %(min)d characters (it has %(length)d).') % {'min': self.min_length, 'length': value_length}) + raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length}) return value def widget_attrs(self, widget): @@ -131,6 +153,12 @@ class CharField(Field): return {'maxlength': str(self.max_length)} class IntegerField(Field): + default_error_messages = { + 'invalid': _(u'Enter a whole number.'), + 'max_value': _(u'Ensure this value is less than or equal to %s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + } + def __init__(self, max_value=None, min_value=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value super(IntegerField, self).__init__(*args, **kwargs) @@ -146,14 +174,20 @@ class IntegerField(Field): try: value = int(str(value)) except (ValueError, TypeError): - raise ValidationError(ugettext(u'Enter a whole number.')) + raise ValidationError(self.error_messages['invalid']) if self.max_value is not None and value > self.max_value: - raise ValidationError(ugettext(u'Ensure this value is less than or equal to %s.') % self.max_value) + raise ValidationError(self.error_messages['max_value'] % self.max_value) if self.min_value is not None and value < self.min_value: - raise ValidationError(ugettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) + raise ValidationError(self.error_messages['min_value'] % self.min_value) return value class FloatField(Field): + default_error_messages = { + 'invalid': _(u'Enter a number.'), + 'max_value': _(u'Ensure this value is less than or equal to %s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + } + def __init__(self, max_value=None, min_value=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value Field.__init__(self, *args, **kwargs) @@ -169,14 +203,23 @@ class FloatField(Field): try: value = float(value) except (ValueError, TypeError): - raise ValidationError(ugettext('Enter a number.')) + raise ValidationError(self.error_messages['invalid']) if self.max_value is not None and value > self.max_value: - raise ValidationError(ugettext('Ensure this value is less than or equal to %s.') % self.max_value) + raise ValidationError(self.error_messages['max_value'] % self.max_value) if self.min_value is not None and value < self.min_value: - raise ValidationError(ugettext('Ensure this value is greater than or equal to %s.') % self.min_value) + raise ValidationError(self.error_messages['min_value'] % self.min_value) return value class DecimalField(Field): + default_error_messages = { + 'invalid': _(u'Enter a number.'), + 'max_value': _(u'Ensure this value is less than or equal to %s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + 'max_digits': _('Ensure that there are no more than %s digits in total.'), + 'max_decimal_places': _('Ensure that there are no more than %s decimal places.'), + 'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.') + } + def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value self.max_digits, self.decimal_places = max_digits, decimal_places @@ -196,20 +239,20 @@ class DecimalField(Field): try: value = Decimal(value) except DecimalException: - raise ValidationError(ugettext('Enter a number.')) + raise ValidationError(self.error_messages['invalid']) pieces = str(value).lstrip("-").split('.') decimals = (len(pieces) == 2) and len(pieces[1]) or 0 digits = len(pieces[0]) if self.max_value is not None and value > self.max_value: - raise ValidationError(ugettext('Ensure this value is less than or equal to %s.') % self.max_value) + raise ValidationError(self.error_messages['max_value'] % self.max_value) if self.min_value is not None and value < self.min_value: - raise ValidationError(ugettext('Ensure this value is greater than or equal to %s.') % self.min_value) + raise ValidationError(self.error_messages['min_value'] % self.min_value) if self.max_digits is not None and (digits + decimals) > self.max_digits: - raise ValidationError(ugettext('Ensure that there are no more than %s digits in total.') % self.max_digits) + raise ValidationError(self.error_messages['max_digits'] % self.max_digits) if self.decimal_places is not None and decimals > self.decimal_places: - raise ValidationError(ugettext('Ensure that there are no more than %s decimal places.') % self.decimal_places) + raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places) if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places): - raise ValidationError(ugettext('Ensure that there are no more than %s digits before the decimal point.') % (self.max_digits - self.decimal_places)) + raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places)) return value DEFAULT_DATE_INPUT_FORMATS = ( @@ -221,6 +264,10 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) class DateField(Field): + default_error_messages = { + 'invalid': _(u'Enter a valid date.'), + } + def __init__(self, input_formats=None, *args, **kwargs): super(DateField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS @@ -242,7 +289,7 @@ class DateField(Field): return datetime.date(*time.strptime(value, format)[:3]) except ValueError: continue - raise ValidationError(ugettext(u'Enter a valid date.')) + raise ValidationError(self.error_messages['invalid']) DEFAULT_TIME_INPUT_FORMATS = ( '%H:%M:%S', # '14:30:59' @@ -250,6 +297,10 @@ DEFAULT_TIME_INPUT_FORMATS = ( ) class TimeField(Field): + default_error_messages = { + 'invalid': _(u'Enter a valid time.') + } + def __init__(self, input_formats=None, *args, **kwargs): super(TimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS @@ -269,7 +320,7 @@ class TimeField(Field): return datetime.time(*time.strptime(value, format)[3:6]) except ValueError: continue - raise ValidationError(ugettext(u'Enter a valid time.')) + raise ValidationError(self.error_messages['invalid']) DEFAULT_DATETIME_INPUT_FORMATS = ( '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' @@ -285,6 +336,9 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( class DateTimeField(Field): widget = DateTimeInput + default_error_messages = { + 'invalid': _(u'Enter a valid date/time.'), + } def __init__(self, input_formats=None, *args, **kwargs): super(DateTimeField, self).__init__(*args, **kwargs) @@ -306,14 +360,14 @@ class DateTimeField(Field): # Input comes from a SplitDateTimeWidget, for example. So, it's two # components: date and time. if len(value) != 2: - raise ValidationError(ugettext(u'Enter a valid date/time.')) + raise ValidationError(self.error_messages['invalid']) value = '%s %s' % tuple(value) for format in self.input_formats: try: return datetime.datetime(*time.strptime(value, format)[:6]) except ValueError: continue - raise ValidationError(ugettext(u'Enter a valid date/time.')) + raise ValidationError(self.error_messages['invalid']) class RegexField(CharField): def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): @@ -322,11 +376,15 @@ class RegexField(CharField): error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ + # error_message is just kept for backwards compatibility: + if error_message: + error_messages = kwargs.get('error_messages') or {} + error_messages['invalid'] = error_message + kwargs['error_messages'] = error_messages super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex - self.error_message = error_message or ugettext(u'Enter a valid value.') def clean(self, value): """ @@ -337,7 +395,7 @@ class RegexField(CharField): if value == u'': return value if not self.regex.search(value): - raise ValidationError(self.error_message) + raise ValidationError(self.error_messages['invalid']) return value email_re = re.compile( @@ -346,9 +404,13 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): + default_error_messages = { + 'invalid': _(u'Enter a valid e-mail address.'), + } + def __init__(self, max_length=None, min_length=None, *args, **kwargs): - RegexField.__init__(self, email_re, max_length, min_length, - ugettext(u'Enter a valid e-mail address.'), *args, **kwargs) + RegexField.__init__(self, email_re, max_length, min_length, *args, + **kwargs) try: from django.conf import settings @@ -372,6 +434,12 @@ class UploadedFile(StrAndUnicode): class FileField(Field): widget = FileInput + default_error_messages = { + 'invalid': _(u"No file was submitted. Check the encoding type on the form."), + 'missing': _(u"No file was submitted."), + 'empty': _(u"The submitted file is empty."), + } + def __init__(self, *args, **kwargs): super(FileField, self).__init__(*args, **kwargs) @@ -382,14 +450,18 @@ class FileField(Field): try: f = UploadedFile(data['filename'], data['content']) except TypeError: - raise ValidationError(ugettext(u"No file was submitted. Check the encoding type on the form.")) + raise ValidationError(self.error_messages['invalid']) except KeyError: - raise ValidationError(ugettext(u"No file was submitted.")) + raise ValidationError(self.error_messages['missing']) if not f.content: - raise ValidationError(ugettext(u"The submitted file is empty.")) + raise ValidationError(self.error_messages['empty']) return f class ImageField(FileField): + default_error_messages = { + 'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."), + } + def clean(self, data): """ Checks that the file-upload field data contains a valid image (GIF, JPG, @@ -410,7 +482,7 @@ class ImageField(FileField): trial_image = Image.open(StringIO(f.content)) trial_image.verify() except Exception: # Python Imaging Library doesn't recognize it as an image - raise ValidationError(ugettext(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image.")) + raise ValidationError(self.error_messages['invalid_image']) return f url_re = re.compile( @@ -422,9 +494,15 @@ url_re = re.compile( r'(?:/?|/\S+)$', re.IGNORECASE) class URLField(RegexField): + default_error_messages = { + 'invalid': _(u'Enter a valid URL.'), + 'invalid_link': _(u'This URL appears to be a broken link.'), + } + def __init__(self, max_length=None, min_length=None, verify_exists=False, validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs): - super(URLField, self).__init__(url_re, max_length, min_length, ugettext(u'Enter a valid URL.'), *args, **kwargs) + super(URLField, self).__init__(url_re, max_length, min_length, *args, + **kwargs) self.verify_exists = verify_exists self.user_agent = validator_user_agent @@ -449,9 +527,9 @@ class URLField(RegexField): req = urllib2.Request(value, None, headers) u = urllib2.urlopen(req) except ValueError: - raise ValidationError(ugettext(u'Enter a valid URL.')) + raise ValidationError(self.error_messages['invalid']) except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError(ugettext(u'This URL appears to be a broken link.')) + raise ValidationError(self.error_messages['invalid_link']) return value class BooleanField(Field): @@ -478,9 +556,14 @@ class NullBooleanField(BooleanField): class ChoiceField(Field): widget = Select + default_error_messages = { + 'invalid_choice': _(u'Select a valid choice. That choice is not one of the available choices.'), + } - def __init__(self, choices=(), required=True, widget=None, label=None, initial=None, help_text=None): - super(ChoiceField, self).__init__(required, widget, label, initial, help_text) + def __init__(self, choices=(), required=True, widget=None, label=None, + initial=None, help_text=None, *args, **kwargs): + super(ChoiceField, self).__init__(required, widget, label, initial, + help_text, *args, **kwargs) self.choices = choices def _get_choices(self): @@ -506,29 +589,33 @@ class ChoiceField(Field): return value valid_values = set([smart_unicode(k) for k, v in self.choices]) if value not in valid_values: - raise ValidationError(ugettext(u'Select a valid choice. That choice is not one of the available choices.')) + raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) return value class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput widget = SelectMultiple + default_error_messages = { + 'invalid_choice': _(u'Select a valid choice. %(value)s is not one of the available choices.'), + 'invalid_list': _(u'Enter a list of values.'), + } def clean(self, value): """ Validates that the input is a list or tuple. """ if self.required and not value: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) elif not self.required and not value: return [] if not isinstance(value, (list, tuple)): - raise ValidationError(ugettext(u'Enter a list of values.')) + raise ValidationError(self.error_messages['invalid_list']) new_value = [smart_unicode(val) for val in value] # Validate that each value in the value list is in self.choices. valid_values = set([smart_unicode(k) for k, v in self.choices]) for val in new_value: if val not in valid_values: - raise ValidationError(ugettext(u'Select a valid choice. %s is not one of the available choices.') % val) + raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) return new_value class ComboField(Field): @@ -571,6 +658,10 @@ class MultiValueField(Field): You'll probably want to use this with MultiWidget. """ + default_error_messages = { + 'invalid': _(u'Enter a list of values.'), + } + def __init__(self, fields=(), *args, **kwargs): super(MultiValueField, self).__init__(*args, **kwargs) # Set 'required' to False on the individual fields, because the @@ -594,18 +685,18 @@ class MultiValueField(Field): if not value or isinstance(value, (list, tuple)): if not value or not [v for v in value if v not in EMPTY_VALUES]: if self.required: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) else: return self.compress([]) else: - raise ValidationError(ugettext(u'Enter a list of values.')) + raise ValidationError(self.error_messages['invalid']) for i, field in enumerate(self.fields): try: field_value = value[i] except IndexError: field_value = None if self.required and field_value in EMPTY_VALUES: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) try: clean_data.append(field.clean(field_value)) except ValidationError, e: @@ -629,8 +720,19 @@ class MultiValueField(Field): raise NotImplementedError('Subclasses must implement this method.') class SplitDateTimeField(MultiValueField): + default_error_messages = { + 'invalid_date': _(u'Enter a valid date.'), + 'invalid_time': _(u'Enter a valid time.'), + } + def __init__(self, *args, **kwargs): - fields = (DateField(), TimeField()) + errors = self.default_error_messages.copy() + if 'error_messages' in kwargs: + errors.update(kwargs['error_messages']) + fields = ( + DateField(error_messages={'invalid': errors['invalid_date']}), + TimeField(error_messages={'invalid': errors['invalid_time']}), + ) super(SplitDateTimeField, self).__init__(fields, *args, **kwargs) def compress(self, data_list): @@ -638,16 +740,18 @@ class SplitDateTimeField(MultiValueField): # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in EMPTY_VALUES: - raise ValidationError(ugettext(u'Enter a valid date.')) + raise ValidationError(self.error_messages['invalid_date']) if data_list[1] in EMPTY_VALUES: - raise ValidationError(ugettext(u'Enter a valid time.')) + raise ValidationError(self.error_messages['invalid_time']) return datetime.datetime.combine(*data_list) return None ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') class IPAddressField(RegexField): + default_error_messages = { + 'invalid': _(u'Enter a valid IPv4 address.'), + } + def __init__(self, *args, **kwargs): - RegexField.__init__(self, ipv4_re, - error_message=ugettext(u'Enter a valid IPv4 address.'), - *args, **kwargs) + super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs) diff --git a/django/newforms/models.py b/django/newforms/models.py index 5b8bc153cc..3a80517816 100644 --- a/django/newforms/models.py +++ b/django/newforms/models.py @@ -6,7 +6,6 @@ and database field objects. from django.utils.translation import ugettext from django.utils.encoding import smart_unicode - from util import ValidationError from forms import BaseForm, SortedDictFromList from fields import Field, ChoiceField, IntegerField @@ -19,7 +18,8 @@ __all__ = ( 'ModelChoiceField', 'ModelMultipleChoiceField', ) -def save_instance(form, instance, fields=None, fail_message='saved', commit=True): +def save_instance(form, instance, fields=None, fail_message='saved', + commit=True): """ Saves bound Form ``form``'s cleaned_data into model instance ``instance``. @@ -29,15 +29,17 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True from django.db import models opts = instance.__class__._meta if form.errors: - raise ValueError("The %s could not be %s because the data didn't validate." % (opts.object_name, fail_message)) + raise ValueError("The %s could not be %s because the data didn't" + " validate." % (opts.object_name, fail_message)) cleaned_data = form.cleaned_data for f in opts.fields: - if not f.editable or isinstance(f, models.AutoField) or not f.name in cleaned_data: + if not f.editable or isinstance(f, models.AutoField) \ + or not f.name in cleaned_data: continue if fields and f.name not in fields: continue f.save_form_data(instance, cleaned_data[f.name]) - # Wrap up the saving of m2m data as a function + # Wrap up the saving of m2m data as a function. def save_m2m(): opts = instance.__class__._meta cleaned_data = form.cleaned_data @@ -47,28 +49,29 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True if f.name in cleaned_data: f.save_form_data(instance, cleaned_data[f.name]) if commit: - # If we are committing, save the instance and the m2m data immediately + # If we are committing, save the instance and the m2m data immediately. instance.save() save_m2m() else: # We're not committing. Add a method to the form to allow deferred - # saving of m2m data + # saving of m2m data. form.save_m2m = save_m2m return instance def make_model_save(model, fields, fail_message): - "Returns the save() method for a Form." + """Returns the save() method for a Form.""" def save(self, commit=True): return save_instance(self, model(), fields, fail_message, commit) return save def make_instance_save(instance, fields, fail_message): - "Returns the save() method for a Form." + """Returns the save() method for a Form.""" def save(self, commit=True): return save_instance(self, instance, fields, fail_message, commit) return save -def form_for_model(model, form=BaseForm, fields=None, formfield_callback=lambda f: f.formfield()): +def form_for_model(model, form=BaseForm, fields=None, + formfield_callback=lambda f: f.formfield()): """ Returns a Form class for the given Django model class. @@ -90,9 +93,11 @@ def form_for_model(model, form=BaseForm, fields=None, formfield_callback=lambda field_list.append((f.name, formfield)) base_fields = SortedDictFromList(field_list) return type(opts.object_name + 'Form', (form,), - {'base_fields': base_fields, '_model': model, 'save': make_model_save(model, fields, 'created')}) + {'base_fields': base_fields, '_model': model, + 'save': make_model_save(model, fields, 'created')}) -def form_for_instance(instance, form=BaseForm, fields=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): +def form_for_instance(instance, form=BaseForm, fields=None, + formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): """ Returns a Form class for the given Django model instance. @@ -117,16 +122,22 @@ def form_for_instance(instance, form=BaseForm, fields=None, formfield_callback=l field_list.append((f.name, formfield)) base_fields = SortedDictFromList(field_list) return type(opts.object_name + 'InstanceForm', (form,), - {'base_fields': base_fields, '_model': model, 'save': make_instance_save(instance, fields, 'changed')}) + {'base_fields': base_fields, '_model': model, + 'save': make_instance_save(instance, fields, 'changed')}) def form_for_fields(field_list): - "Returns a Form class for the given list of Django database field instances." - fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list if f.editable]) + """ + Returns a Form class for the given list of Django database field instances. + """ + fields = SortedDictFromList([(f.name, f.formfield()) + for f in field_list if f.editable]) return type('FormForFields', (BaseForm,), {'base_fields': fields}) class QuerySetIterator(object): def __init__(self, queryset, empty_label, cache_choices): - self.queryset, self.empty_label, self.cache_choices = queryset, empty_label, cache_choices + self.queryset = queryset + self.empty_label = empty_label + self.cache_choices = cache_choices def __iter__(self): if self.empty_label is not None: @@ -138,11 +149,13 @@ class QuerySetIterator(object): self.queryset._result_cache = None class ModelChoiceField(ChoiceField): - "A ChoiceField whose choices are a model QuerySet." + """A ChoiceField whose choices are a model QuerySet.""" # This class is a subclass of ChoiceField for purity, but it doesn't # actually use any of ChoiceField's implementation. + def __init__(self, queryset, empty_label=u"---------", cache_choices=False, - required=True, widget=Select, label=None, initial=None, help_text=None): + required=True, widget=Select, label=None, initial=None, + help_text=None): self.queryset = queryset self.empty_label = empty_label self.cache_choices = cache_choices @@ -162,7 +175,8 @@ class ModelChoiceField(ChoiceField): # *each* time _get_choices() is called (and, thus, each time # self.choices is accessed) so that we can ensure the QuerySet has not # been consumed. - return QuerySetIterator(self.queryset, self.empty_label, self.cache_choices) + return QuerySetIterator(self.queryset, self.empty_label, + self.cache_choices) def _set_choices(self, value): # This method is copied from ChoiceField._set_choices(). It's necessary @@ -179,16 +193,20 @@ class ModelChoiceField(ChoiceField): try: value = self.queryset.model._default_manager.get(pk=value) except self.queryset.model.DoesNotExist: - raise ValidationError(ugettext(u'Select a valid choice. That choice is not one of the available choices.')) + raise ValidationError(ugettext(u'Select a valid choice. That' + u' choice is not one of the' + u' available choices.')) return value class ModelMultipleChoiceField(ModelChoiceField): - "A MultipleChoiceField whose choices are a model QuerySet." + """A MultipleChoiceField whose choices are a model QuerySet.""" hidden_widget = MultipleHiddenInput + def __init__(self, queryset, cache_choices=False, required=True, - widget=SelectMultiple, label=None, initial=None, help_text=None): - super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices, - required, widget, label, initial, help_text) + widget=SelectMultiple, label=None, initial=None, + help_text=None): + super(ModelMultipleChoiceField, self).__init__(queryset, None, + cache_choices, required, widget, label, initial, help_text) def clean(self, value): if self.required and not value: @@ -202,7 +220,9 @@ class ModelMultipleChoiceField(ModelChoiceField): try: obj = self.queryset.model._default_manager.get(pk=val) except self.queryset.model.DoesNotExist: - raise ValidationError(ugettext(u'Select a valid choice. %s is not one of the available choices.') % val) + raise ValidationError(ugettext(u'Select a valid choice. %s is' + u' not one of the available' + u' choices.') % val) else: final_values.append(obj) return final_values diff --git a/django/newforms/util.py b/django/newforms/util.py index e19d894397..490fbcaed0 100644 --- a/django/newforms/util.py +++ b/django/newforms/util.py @@ -42,13 +42,18 @@ class ErrorList(list, StrAndUnicode): if not self: return u'' return u'\n'.join([u'* %s' % force_unicode(e) for e in self]) + def __repr__(self): + return repr([force_unicode(e) for e in self]) + class ValidationError(Exception): def __init__(self, message): - "ValidationError can be passed a string or a list." + """ + ValidationError can be passed any object that can be printed (usually + a string) or a list of objects. + """ if isinstance(message, list): self.messages = ErrorList([smart_unicode(msg) for msg in message]) else: - assert isinstance(message, (basestring, Promise)), ("%s should be a basestring or lazy translation" % repr(message)) message = smart_unicode(message) self.messages = ErrorList([message]) @@ -58,4 +63,3 @@ class ValidationError(Exception): # AttributeError: ValidationError instance has no attribute 'args' # See http://www.python.org/doc/current/tut/node10.html#handling return repr(self.messages) - diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index f1fcf5fe90..d2d4b9e508 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -1,11 +1,12 @@ -"Default variable filters" +"""Default variable filters.""" + +import re +import random as random_module from django.template import Variable, Library from django.conf import settings from django.utils.translation import ugettext, ungettext -from django.utils.encoding import force_unicode, smart_str, iri_to_uri -import re -import random as random_module +from django.utils.encoding import force_unicode, iri_to_uri register = Library() @@ -36,39 +37,48 @@ def stringfilter(func): def addslashes(value): - "Adds slashes - useful for passing strings to JavaScript, for example." + """Adds slashes - useful for passing strings to JavaScript, for example.""" return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") addslashes = stringfilter(addslashes) def capfirst(value): - "Capitalizes the first character of the value" + """Capitalizes the first character of the value.""" return value and value[0].upper() + value[1:] capfirst = stringfilter(capfirst) def fix_ampersands(value): - "Replaces ampersands with ``&`` entities" + """Replaces ampersands with ``&`` entities.""" from django.utils.html import fix_ampersands return fix_ampersands(value) fix_ampersands = stringfilter(fix_ampersands) def floatformat(text, arg=-1): """ - If called without an argument, displays a floating point - number as 34.2 -- but only if there's a point to be displayed. - With a positive numeric argument, it displays that many decimal places - always. - With a negative numeric argument, it will display that many decimal - places -- but only if there's places to be displayed. - Examples: + Displays a float to a specified number of decimal places. + + If called without an argument, it displays the floating point number with + one decimal place -- but only if there's a decimal place to be displayed: * num1 = 34.23234 * num2 = 34.00000 - * num1|floatformat results in 34.2 - * num2|floatformat is 34 - * num1|floatformat:3 is 34.232 - * num2|floatformat:3 is 34.000 - * num1|floatformat:-3 is 34.232 - * num2|floatformat:-3 is 34 + * num3 = 34.26000 + * {{ num1|floatformat }} displays "34.2" + * {{ num2|floatformat }} displays "34" + * {{ num3|floatformat }} displays "34.3" + + If arg is positive, it will always display exactly arg number of decimal + places: + + * {{ num1|floatformat:3 }} displays "34.232" + * {{ num2|floatformat:3 }} displays "34.000" + * {{ num3|floatformat:3 }} displays "34.260" + + If arg is negative, it will display arg number of decimal places -- but + only if there are places to be displayed: + + * {{ num1|floatformat:"-3" }} displays "34.232" + * {{ num2|floatformat:"-3" }} displays "34" + * {{ num3|floatformat:"-3" }} displays "34.260" """ try: f = float(text) @@ -86,15 +96,16 @@ def floatformat(text, arg=-1): return formatstr % f def iriencode(value): - "Escapes an IRI value for use in a URL" + """Escapes an IRI value for use in a URL.""" return force_unicode(iri_to_uri(value)) iriencode = stringfilter(iriencode) def linenumbers(value): - "Displays text with line numbers" + """Displays text with line numbers.""" from django.utils.html import escape lines = value.split(u'\n') - # Find the maximum width of the line count, for use with zero padding string format command + # Find the maximum width of the line count, for use with zero padding + # string format command. width = unicode(len(unicode(len(lines)))) for i, line in enumerate(lines): lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) @@ -102,22 +113,24 @@ def linenumbers(value): linenumbers = stringfilter(linenumbers) def lower(value): - "Converts a string into all lowercase" + """Converts a string into all lowercase.""" return value.lower() lower = stringfilter(lower) def make_list(value): """ - Returns the value turned into a list. For an integer, it's a list of - digits. For a string, it's a list of characters. + Returns the value turned into a list. + + For an integer, it's a list of digits. + For a string, it's a list of characters. """ return list(value) make_list = stringfilter(make_list) def slugify(value): """ - Normalizes string, converts to lowercase, removes non-alpha chars and - converts spaces to hyphens. + Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. """ import unicodedata value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') @@ -127,7 +140,8 @@ slugify = stringfilter(slugify) def stringformat(value, arg): """ - Formats the variable according to the argument, a string formatting specifier. + Formats the variable according to the arg, a string formatting specifier. + This specifier uses Python string formating syntax, with the exception that the leading "%" is dropped. @@ -140,29 +154,29 @@ def stringformat(value, arg): return u"" def title(value): - "Converts a string into titlecase" + """Converts a string into titlecase.""" return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) title = stringfilter(title) def truncatewords(value, arg): """ - Truncates a string after a certain number of words + Truncates a string after a certain number of words. - Argument: Number of words to truncate after + Argument: Number of words to truncate after. """ from django.utils.text import truncate_words try: length = int(arg) - except ValueError: # invalid literal for int() + except ValueError: # Invalid literal for int(). return value # Fail silently. return truncate_words(value, length) truncatewords = stringfilter(truncatewords) def truncatewords_html(value, arg): """ - Truncates HTML after a certain number of words + Truncates HTML after a certain number of words. - Argument: Number of words to truncate after + Argument: Number of words to truncate after. """ from django.utils.text import truncate_html_words try: @@ -173,26 +187,26 @@ def truncatewords_html(value, arg): truncatewords_html = stringfilter(truncatewords_html) def upper(value): - "Converts a string into all uppercase" + """Converts a string into all uppercase.""" return value.upper() upper = stringfilter(upper) def urlencode(value): - "Escapes a value for use in a URL" + """Escapes a value for use in a URL.""" from django.utils.http import urlquote return urlquote(value) urlencode = stringfilter(urlencode) def urlize(value): - "Converts URLs in plain text into clickable links" + """Converts URLs in plain text into clickable links.""" from django.utils.html import urlize return urlize(value, nofollow=True) urlize = stringfilter(urlize) def urlizetrunc(value, limit): """ - Converts URLs into clickable links, truncating URLs to the given character limit, - and adding 'rel=nofollow' attribute to discourage spamming. + Converts URLs into clickable links, truncating URLs to the given character + limit, and adding 'rel=nofollow' attribute to discourage spamming. Argument: Length to truncate URLs to. """ @@ -201,13 +215,13 @@ def urlizetrunc(value, limit): urlizetrunc = stringfilter(urlizetrunc) def wordcount(value): - "Returns the number of words" + """Returns the number of words.""" return len(value.split()) wordcount = stringfilter(wordcount) def wordwrap(value, arg): """ - Wraps words at specified line length + Wraps words at specified line length. Argument: number of characters to wrap the text at. """ @@ -217,29 +231,29 @@ wordwrap = stringfilter(wordwrap) def ljust(value, arg): """ - Left-aligns the value in a field of a given width + Left-aligns the value in a field of a given width. - Argument: field size + Argument: field size. """ return value.ljust(int(arg)) ljust = stringfilter(ljust) def rjust(value, arg): """ - Right-aligns the value in a field of a given width + Right-aligns the value in a field of a given width. - Argument: field size + Argument: field size. """ return value.rjust(int(arg)) rjust = stringfilter(rjust) def center(value, arg): - "Centers the value in a field of a given width" + """Centers the value in a field of a given width.""" return value.center(int(arg)) center = stringfilter(center) def cut(value, arg): - "Removes all values of arg from the given string" + """Removes all values of arg from the given string.""" return value.replace(arg, u'') cut = stringfilter(cut) @@ -272,7 +286,7 @@ def linebreaksbr(value): linebreaksbr = stringfilter(linebreaksbr) def removetags(value, tags): - "Removes a space separated list of [X]HTML tags from the output" + """Removes a space separated list of [X]HTML tags from the output.""" tags = [re.escape(tag) for tag in tags.split()] tags_re = u'(%s)' % u'|'.join(tags) starttag_re = re.compile(ur'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U) @@ -283,7 +297,7 @@ def removetags(value, tags): removetags = stringfilter(removetags) def striptags(value): - "Strips all [X]HTML tags" + """Strips all [X]HTML tags.""" from django.utils.html import strip_tags return strip_tags(value) striptags = stringfilter(striptags) @@ -314,29 +328,29 @@ def dictsortreversed(value, arg): return [item[1] for item in decorated] def first(value): - "Returns the first item in a list" + """Returns the first item in a list.""" try: return value[0] except IndexError: return u'' def join(value, arg): - "Joins a list with a string, like Python's ``str.join(list)``" + """Joins a list with a string, like Python's ``str.join(list)``.""" try: return arg.join(map(force_unicode, value)) except AttributeError: # fail silently but nicely return value def length(value): - "Returns the length of the value - useful for lists" + """Returns the length of the value - useful for lists.""" return len(value) def length_is(value, arg): - "Returns a boolean of whether the value's length is the argument" + """Returns a boolean of whether the value's length is the argument.""" return len(value) == int(arg) def random(value): - "Returns a random item from the list" + """Returns a random item from the list.""" return random_module.choice(value) def slice_(value, arg): @@ -416,7 +430,7 @@ def unordered_list(value): sublist = '' sublist_item = None if isinstance(title, (list, tuple)): - sublist_item = title + sublist_item = title title = '' elif i < list_length - 1: next_item = list_[i+1] @@ -424,7 +438,7 @@ def unordered_list(value): # The next item is a sub-list. sublist_item = next_item # We've processed the next item now too. - i += 1 + i += 1 if sublist_item: sublist = _helper(sublist_item, tabs+1) sublist = '\n%s\n%s' % (indent, sublist, @@ -433,7 +447,7 @@ def unordered_list(value): sublist)) i += 1 return '\n'.join(output) - value, converted = convert_old_style_list(value) + value, converted = convert_old_style_list(value) return _helper(value) ################### @@ -441,7 +455,7 @@ def unordered_list(value): ################### def add(value, arg): - "Adds the arg to the value" + """Adds the arg to the value.""" return int(value) + int(arg) def get_digit(value, arg): @@ -468,7 +482,7 @@ def get_digit(value, arg): ################### def date(value, arg=None): - "Formats a date according to the given format" + """Formats a date according to the given format.""" from django.utils.dateformat import format if not value: return u'' @@ -477,7 +491,7 @@ def date(value, arg=None): return format(value, arg) def time(value, arg=None): - "Formats a time according to the given format" + """Formats a time according to the given format.""" from django.utils.dateformat import time_format if value in (None, u''): return u'' @@ -486,7 +500,7 @@ def time(value, arg=None): return time_format(value, arg) def timesince(value, arg=None): - 'Formats a date as the time since that date (i.e. "4 days, 6 hours")' + """Formats a date as the time since that date (i.e. "4 days, 6 hours").""" from django.utils.timesince import timesince if not value: return u'' @@ -495,7 +509,7 @@ def timesince(value, arg=None): return timesince(value) def timeuntil(value, arg=None): - 'Formats a date as the time until that date (i.e. "4 days, 6 hours")' + """Formats a date as the time until that date (i.e. "4 days, 6 hours").""" from django.utils.timesince import timesince from datetime import datetime if not value: @@ -509,17 +523,17 @@ def timeuntil(value, arg=None): ################### def default(value, arg): - "If value is unavailable, use given default" + """If value is unavailable, use given default.""" return value or arg def default_if_none(value, arg): - "If value is None, use given default" + """If value is None, use given default.""" if value is None: return arg return value def divisibleby(value, arg): - "Returns true if the value is devisible by the argument" + """Returns True if the value is devisible by the argument.""" return int(value) % int(arg) == 0 def yesno(value, arg=None): @@ -544,7 +558,8 @@ def yesno(value, arg=None): return value # Invalid arg. try: yes, no, maybe = bits - except ValueError: # unpack list of wrong size (no "maybe" value provided) + except ValueError: + # Unpack list of wrong size (no "maybe" value provided). yes, no, maybe = bits[0], bits[1], bits[1] if value is None: return maybe @@ -558,8 +573,8 @@ def yesno(value, arg=None): def filesizeformat(bytes): """ - Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 - bytes, etc). + Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, + 102 bytes, etc). """ try: bytes = float(bytes) @@ -576,10 +591,26 @@ def filesizeformat(bytes): def pluralize(value, arg=u's'): """ - Returns a plural suffix if the value is not 1, for '1 vote' vs. '2 votes' - By default, 's' is used as a suffix; if an argument is provided, that string - is used instead. If the provided argument contains a comma, the text before - the comma is used for the singular case. + Returns a plural suffix if the value is not 1. By default, 's' is used as + the suffix: + + * If value is 0, vote{{ value|plurlize }} displays "0 votes". + * If value is 1, vote{{ value|plurlize }} displays "1 vote". + * If value is 2, vote{{ value|plurlize }} displays "2 votes". + + If an argument is provided, that string is used instead: + + * If value is 0, class{{ value|plurlize:"es" }} displays "0 classes". + * If value is 1, class{{ value|plurlize:"es" }} displays "1 class". + * If value is 2, class{{ value|plurlize:"es" }} displays "2 classes". + + If the provided argument contains a comma, the text before the comma is + used for the singular case and the text after the comma is used for the + plural case: + + * If value is 0, cand{{ value|plurlize:"y,ies" }} displays "0 candies". + * If value is 1, cand{{ value|plurlize:"y,ies" }} displays "1 candy". + * If value is 2, cand{{ value|plurlize:"y,ies" }} displays "2 candies". """ if not u',' in arg: arg = u',' + arg @@ -591,23 +622,23 @@ def pluralize(value, arg=u's'): try: if int(value) != 1: return plural_suffix - except ValueError: # invalid string that's not a number + except ValueError: # Invalid string that's not a number. pass - except TypeError: # value isn't a string or a number; maybe it's a list? + except TypeError: # Value isn't a string or a number; maybe it's a list? try: if len(value) != 1: return plural_suffix - except TypeError: # len() of unsized object + except TypeError: # len() of unsized object. pass return singular_suffix def phone2numeric(value): - "Takes a phone number and converts it in to its numerical equivalent" + """Takes a phone number and converts it in to its numerical equivalent.""" from django.utils.text import phone2numeric return phone2numeric(value) def pprint(value): - "A wrapper around pprint.pprint -- for debugging, really" + """A wrapper around pprint.pprint -- for debugging, really.""" from pprint import pformat try: return pformat(value) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 2ba23a0465..d3c18897eb 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1,4 +1,12 @@ -"Default tags used by the template system, available to all templates." +"""Default tags used by the template system, available to all templates.""" + +import sys +import re +from itertools import cycle as itertools_cycle +try: + reversed +except NameError: + from django.utils.itercompat import reversed # Python 2.3 fallback from django.template import Node, NodeList, Template, Context, Variable from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END @@ -6,13 +14,6 @@ from django.template import get_library, Library, InvalidTemplateLibrary from django.conf import settings from django.utils.encoding import smart_str, smart_unicode from django.utils.itercompat import groupby -import sys -import re - -try: - reversed -except NameError: - from django.utils.itercompat import reversed # Python 2.3 fallback register = Library() @@ -22,14 +23,11 @@ class CommentNode(Node): class CycleNode(Node): def __init__(self, cyclevars, variable_name=None): - self.cyclevars = cyclevars - self.cyclevars_len = len(cyclevars) - self.counter = -1 + self.cycle_iter = itertools_cycle(cyclevars) self.variable_name = variable_name def render(self, context): - self.counter += 1 - value = self.cyclevars[self.counter % self.cyclevars_len] + value = self.cycle_iter.next() value = Variable(value).resolve(context) if self.variable_name: context[self.variable_name] = value @@ -49,7 +47,7 @@ class FilterNode(Node): def render(self, context): output = self.nodelist.render(context) - # apply filters + # Apply filters. context.update({'var': output}) filtered = self.filter_expr.resolve(context) context.pop() @@ -81,7 +79,8 @@ class ForNode(Node): else: reversed = '' return "" % \ - (', '.join( self.loopvars ), self.sequence, len(self.nodelist_loop), reversed) + (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop), + reversed) def __iter__(self): for node in self.nodelist_loop: @@ -115,19 +114,20 @@ class ForNode(Node): unpack = len(self.loopvars) > 1 for i, item in enumerate(values): context['forloop'] = { - # shortcuts for current loop iteration number + # Shortcuts for current loop iteration number. 'counter0': i, 'counter': i+1, - # reverse counter iteration numbers + # Reverse counter iteration numbers. 'revcounter': len_values - i, 'revcounter0': len_values - i - 1, - # boolean values designating first and last times through loop + # Boolean values designating first and last times through loop. 'first': (i == 0), 'last': (i == len_values - 1), 'parentloop': parentloop, } if unpack: - # If there are multiple loop variables, unpack the item into them. + # If there are multiple loop variables, unpack the item into + # them. context.update(dict(zip(self.loopvars, item))) else: context[self.loopvars[0]] = item @@ -154,8 +154,8 @@ class IfChangedNode(Node): self._last_seen = None try: if self._varlist: - # Consider multiple parameters. - # This automatically behaves like a OR evaluation of the multiple variables. + # Consider multiple parameters. This automatically behaves + # like an OR evaluation of the multiple variables. compare_to = [var.resolve(context) for var in self._varlist] else: compare_to = self.nodelist.render(context) @@ -249,13 +249,17 @@ class RegroupNode(Node): def render(self, context): obj_list = self.target.resolve(context, True) - if obj_list == None: # target_var wasn't found in context; fail silently + if obj_list == None: + # target variable wasn't found in context; fail silently. context[self.var_name] = [] return '' - # List of dictionaries in the format + # List of dictionaries in the format: # {'grouper': 'key', 'list': [list of contents]}. - context[self.var_name] = [{'grouper':key, 'list':list(val)} for key, val in - groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True))] + context[self.var_name] = [ + {'grouper': key, 'list': list(val)} + for key, val in + groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True)) + ] return '' def include_is_allowed(filepath): @@ -339,13 +343,15 @@ class URLNode(Node): def render(self, context): from django.core.urlresolvers import reverse, NoReverseMatch args = [arg.resolve(context) for arg in self.args] - kwargs = dict([(smart_str(k,'ascii'), v.resolve(context)) for k, v in self.kwargs.items()]) + kwargs = dict([(smart_str(k,'ascii'), v.resolve(context)) + for k, v in self.kwargs.items()]) try: return reverse(self.view_name, args=args, kwargs=kwargs) except NoReverseMatch: try: project_name = settings.SETTINGS_MODULE.split('.')[0] - return reverse(project_name + '.' + self.view_name, args=args, kwargs=kwargs) + return reverse(project_name + '.' + self.view_name, + args=args, kwargs=kwargs) except NoReverseMatch: return '' @@ -389,7 +395,7 @@ class WithNode(Node): #@register.tag def comment(parser, token): """ - Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` + Ignores everything between ``{% comment %}`` and ``{% endcomment %}``. """ parser.skip_past('endcomment') return CommentNode() @@ -398,7 +404,7 @@ comment = register.tag(comment) #@register.tag def cycle(parser, token): """ - Cycle among the given strings each time this tag is encountered + Cycles among the given strings each time this tag is encountered. Within a loop, cycles among the given strings each time through the loop:: @@ -417,14 +423,14 @@ def cycle(parser, token): ... You can use any number of values, seperated by spaces. Commas can also - be used to separate values; if a comma is used, the cycle values are + be used to separate values; if a comma is used, the cycle values are interpreted as literal strings. """ - # Note: This returns the exact same node on each {% cycle name %} call; that - # is, the node object returned from {% cycle a b c as name %} and the one - # returned from {% cycle name %} are the exact same object. This shouldn't - # cause problems (heh), but if it does, now you know. + # Note: This returns the exact same node on each {% cycle name %} call; + # that is, the node object returned from {% cycle a b c as name %} and the + # one returned from {% cycle name %} are the exact same object. This + # shouldn't cause problems (heh), but if it does, now you know. # # Ugly hack warning: this stuffs the named template dict into parser so # that names are only unique within each template (as opposed to using @@ -442,10 +448,11 @@ def cycle(parser, token): args[1:2] = ['"%s"' % arg for arg in args[1].split(",")] if len(args) == 2: - # {% cycle foo %} case + # {% cycle foo %} case. name = args[1] if not hasattr(parser, '_namedCycleNodes'): - raise TemplateSyntaxError("No named cycles in template: '%s' is not defined" % name) + raise TemplateSyntaxError("No named cycles in template." + " '%s' is not defined" % name) if not name in parser._namedCycleNodes: raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) return parser._namedCycleNodes[name] @@ -463,7 +470,8 @@ cycle = register.tag(cycle) def debug(parser, token): """ - Output a whole load of debugging information, including the current context and imported modules. + Outputs a whole load of debugging information, including the current + context and imported modules. Sample usage:: @@ -477,7 +485,7 @@ debug = register.tag(debug) #@register.tag(name="filter") def do_filter(parser, token): """ - Filter the contents of the blog through variable filters. + Filters the contents of the blog through variable filters. Filters can also be piped through each other, and they can have arguments -- just like in variable syntax. @@ -526,14 +534,15 @@ def firstof(parser, token): """ bits = token.split_contents()[1:] if len(bits) < 1: - raise TemplateSyntaxError, "'firstof' statement requires at least one argument" + raise TemplateSyntaxError("'firstof' statement requires at least one" + " argument") return FirstOfNode(bits) firstof = register.tag(firstof) #@register.tag(name="for") def do_for(parser, token): """ - Loop over each item in an array. + Loops over each item in an array. For example, to display a list of athletes given ``athlete_list``:: @@ -545,9 +554,9 @@ def do_for(parser, token): You can loop over a list in reverse by using ``{% for obj in list reversed %}``. - + You can also unpack multiple values from a two-dimensional array:: - + {% for key,value in dict.items %} {{ key }}: {{ value }} {% endfor %} @@ -572,17 +581,20 @@ def do_for(parser, token): """ bits = token.contents.split() if len(bits) < 4: - raise TemplateSyntaxError, "'for' statements should have at least four words: %s" % token.contents + raise TemplateSyntaxError("'for' statements should have at least four" + " words: %s" % token.contents) reversed = bits[-1] == 'reversed' in_index = reversed and -3 or -2 if bits[in_index] != 'in': - raise TemplateSyntaxError, "'for' statements should use the format 'for x in y': %s" % token.contents + raise TemplateSyntaxError("'for' statements should use the format" + " 'for x in y': %s" % token.contents) loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',') for var in loopvars: if not var or ' ' in var: - raise TemplateSyntaxError, "'for' tag received an invalid argument: %s" % token.contents + raise TemplateSyntaxError("'for' tag received an invalid argument:" + " %s" % token.contents) sequence = parser.compile_filter(bits[in_index+1]) nodelist_loop = parser.parse(('endfor',)) @@ -607,7 +619,7 @@ def do_ifequal(parser, token, negate): #@register.tag def ifequal(parser, token): """ - Output the contents of the block if the two arguments equal each other. + Outputs the contents of the block if the two arguments equal each other. Examples:: @@ -626,7 +638,10 @@ ifequal = register.tag(ifequal) #@register.tag def ifnotequal(parser, token): - """Output the contents of the block if the two arguments are not equal. See ifequal.""" + """ + Outputs the contents of the block if the two arguments are not equal. + See ifequal. + """ return do_ifequal(parser, token, True) ifnotequal = register.tag(ifnotequal) @@ -635,9 +650,7 @@ def do_if(parser, token): """ The ``{% if %}`` tag evaluates a variable, and if that variable is "true" (i.e. exists, is not empty, and is not a false boolean value) the contents - of the block are output: - - :: + of the block are output:: {% if athlete_list %} Number of athletes: {{ athlete_list|count }} @@ -648,8 +661,8 @@ def do_if(parser, token): In the above, if ``athlete_list`` is not empty, the number of athletes will be displayed by the ``{{ athlete_list|count }}`` variable. - As you can see, the ``if`` tag can take an option ``{% else %}`` clause that - will be displayed if the test fails. + As you can see, the ``if`` tag can take an option ``{% else %}`` clause + that will be displayed if the test fails. ``if`` tags may use ``or``, ``and`` or ``not`` to test a number of variables or to negate a given variable:: @@ -674,9 +687,9 @@ def do_if(parser, token): There are some athletes and absolutely no coaches. {% endif %} - ``if`` tags do not allow ``and`` and ``or`` clauses with the same - tag, because the order of logic would be ambigous. For example, - this is invalid:: + ``if`` tags do not allow ``and`` and ``or`` clauses with the same tag, + because the order of logic would be ambigous. For example, this is + invalid:: {% if athlete_list and coach_list or cheerleader_list %} @@ -692,8 +705,8 @@ def do_if(parser, token): bits = token.contents.split() del bits[0] if not bits: - raise TemplateSyntaxError, "'if' statement requires at least one argument" - # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] + raise TemplateSyntaxError("'if' statement requires at least one argument") + # Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] bitstr = ' '.join(bits) boolpairs = bitstr.split(' and ') boolvars = [] @@ -728,13 +741,13 @@ do_if = register.tag("if", do_if) #@register.tag def ifchanged(parser, token): """ - Check if a value has changed from the last iteration of a loop. + Checks if a value has changed from the last iteration of a loop. The 'ifchanged' block tag is used within a loop. It has two possible uses. 1. Checks its own rendered contents against its previous state and only - displays the content if it has changed. For example, this displays a list of - days, only displaying the month if it changes:: + displays the content if it has changed. For example, this displays a + list of days, only displaying the month if it changes::

Archive for {{ year }}

@@ -743,9 +756,9 @@ def ifchanged(parser, token): {{ date|date:"j" }} {% endfor %} - 2. If given a variable, check whether that variable has changed. For example, the - following shows the date every time it changes, but only shows the hour if both - the hour and the date have changed:: + 2. If given a variable, check whether that variable has changed. + For example, the following shows the date every time it changes, but + only shows the hour if both the hour and the date have changed:: {% for date in days %} {% ifchanged date.date %} {{ date.date }} {% endifchanged %} @@ -763,7 +776,7 @@ ifchanged = register.tag(ifchanged) #@register.tag def ssi(parser, token): """ - Output the contents of a given file into the page. + Outputs the contents of a given file into the page. Like a simple "include" tag, the ``ssi`` tag includes the contents of another file -- which must be specified using an absolute page -- @@ -779,21 +792,24 @@ def ssi(parser, token): bits = token.contents.split() parsed = False if len(bits) not in (2, 3): - raise TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included" + raise TemplateSyntaxError("'ssi' tag takes one argument: the path to" + " the file to be included") if len(bits) == 3: if bits[2] == 'parsed': parsed = True else: - raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] + raise TemplateSyntaxError("Second (optional) argument to %s tag" + " must be 'parsed'" % bits[0]) return SsiNode(bits[1], parsed) ssi = register.tag(ssi) #@register.tag def load(parser, token): """ - Load a custom template tag set. + Loads a custom template tag set. - For example, to load the template tags in ``django/templatetags/news/photos.py``:: + For example, to load the template tags in + ``django/templatetags/news/photos.py``:: {% load news.photos %} """ @@ -804,14 +820,15 @@ def load(parser, token): lib = get_library("django.templatetags.%s" % taglib) parser.add_library(lib) except InvalidTemplateLibrary, e: - raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e) + raise TemplateSyntaxError("'%s' is not a valid tag library: %s" % + (taglib, e)) return LoadNode() load = register.tag(load) #@register.tag def now(parser, token): """ - Display the date, formatted according to the given string. + Displays the date, formatted according to the given string. Uses the same format as PHP's ``date()`` function; see http://php.net/date for all the possible values. @@ -830,7 +847,7 @@ now = register.tag(now) #@register.tag def regroup(parser, token): """ - Regroup a list of alike objects by a common attribute. + Regroups a list of alike objects by a common attribute. This complex tag is best illustrated by use of an example: say that ``people`` is a list of ``Person`` objects that have ``first_name``, @@ -868,8 +885,8 @@ def regroup(parser, token): Note that `{% regroup %}`` does not work when the list to be grouped is not sorted by the key you are grouping by! This means that if your list of - people was not sorted by gender, you'd need to make sure it is sorted before - using it, i.e.:: + people was not sorted by gender, you'd need to make sure it is sorted + before using it, i.e.:: {% regroup people|dictsort:"gender" by gender as grouped %} @@ -879,10 +896,11 @@ def regroup(parser, token): raise TemplateSyntaxError, "'regroup' tag takes five arguments" target = parser.compile_filter(firstbits[1]) if firstbits[2] != 'by': - raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" + raise TemplateSyntaxError("second argument to 'regroup' tag must be 'by'") lastbits_reversed = firstbits[3][::-1].split(None, 2) if lastbits_reversed[1][::-1] != 'as': - raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" + raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must" + " be 'as'") expression = parser.compile_filter(lastbits_reversed[2][::-1]) @@ -892,8 +910,7 @@ regroup = register.tag(regroup) def spaceless(parser, token): """ - Removes whitespace between HTML tags. This includes tab - characters and newlines. + Removes whitespace between HTML tags, including tab and newline characters. Example usage:: @@ -907,8 +924,8 @@ def spaceless(parser, token):

Foo

- Only space between *tags* is normalized -- not space between tags and text. In - this example, the space around ``Hello`` won't be stripped:: + Only space between *tags* is normalized -- not space between tags and text. + In this example, the space around ``Hello`` won't be stripped:: {% spaceless %} @@ -924,7 +941,7 @@ spaceless = register.tag(spaceless) #@register.tag def templatetag(parser, token): """ - Output one of the bits used to compose template tags. + Outputs one of the bits used to compose template tags. Since the template system has no concept of "escaping", to display one of the bits used in template tags, you must use the ``{% templatetag %}`` tag. @@ -949,8 +966,9 @@ def templatetag(parser, token): raise TemplateSyntaxError, "'templatetag' statement takes one argument" tag = bits[1] if tag not in TemplateTagNode.mapping: - raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ - (tag, TemplateTagNode.mapping.keys()) + raise TemplateSyntaxError("Invalid templatetag argument: '%s'." + " Must be one of: %s" % + (tag, TemplateTagNode.mapping.keys())) return TemplateTagNode(tag) templatetag = register.tag(templatetag) @@ -958,7 +976,8 @@ def url(parser, token): """ Returns an absolute URL matching given view with its parameters. - This is a way to define links that aren't tied to a particular URL configuration:: + This is a way to define links that aren't tied to a particular URL + configuration:: {% url path.to.some_view arg1,arg2,name1=value1 %} @@ -986,7 +1005,8 @@ def url(parser, token): """ bits = token.contents.split(' ', 2) if len(bits) < 2: - raise TemplateSyntaxError, "'%s' takes at least one argument (path to a view)" % bits[0] + raise TemplateSyntaxError("'%s' takes at least one argument" + " (path to a view)" % bits[0]) args = [] kwargs = {} if len(bits) > 2: @@ -1011,8 +1031,8 @@ def widthratio(parser, token): Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in - the above example will be 88 pixels wide (because 175/200 = .875; .875 * - 100 = 87.5 which is rounded up to 88). + the above example will be 88 pixels wide (because 175/200 = .875; + .875 * 100 = 87.5 which is rounded up to 88). """ bits = token.contents.split() if len(bits) != 4: @@ -1029,7 +1049,7 @@ widthratio = register.tag(widthratio) #@register.tag def do_with(parser, token): """ - Add a value to the context (inside of this block) for caching and easy + Adds a value to the context (inside of this block) for caching and easy access. For example:: @@ -1040,7 +1060,8 @@ def do_with(parser, token): """ bits = list(token.split_contents()) if len(bits) != 4 or bits[2] != "as": - raise TemplateSyntaxError, "%r expected format is 'value as name'" % bits[0] + raise TemplateSyntaxError("%r expected format is 'value as name'" % + bits[0]) var = parser.compile_filter(bits[1]) name = bits[3] nodelist = parser.parse(('endwith',)) diff --git a/django/test/testcases.py b/django/test/testcases.py index 6b7714ec7b..732e713d4a 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -146,7 +146,7 @@ class TestCase(unittest.TestCase): " context %d does not contain the" " error '%s' (actual errors: %s)" % (field, form, i, err, - list(field_errors))) + repr(field_errors))) elif field in context[form].fields: self.fail("The field '%s' on form '%s' in context %d" " contains no errors" % (field, form, i)) diff --git a/django/utils/cache.py b/django/utils/cache.py index f192e1115c..ae4de6dd87 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -13,17 +13,18 @@ into account when building its cache key. Requests with the same path but different header content for headers named in "Vary" need to get different cache keys to prevent delivery of wrong content. -A example: i18n middleware would need to distinguish caches by the +An example: i18n middleware would need to distinguish caches by the "Accept-language" header. """ import md5 import re import time -from email.Utils import formatdate + from django.conf import settings from django.core.cache import cache from django.utils.encoding import smart_str, iri_to_uri +from django.utils.http import http_date cc_delim_re = re.compile(r'\s*,\s*') @@ -40,7 +41,7 @@ def patch_cache_control(response, **kwargs): str() to it. """ def dictitem(s): - t = s.split('=',1) + t = s.split('=', 1) if len(t) > 1: return (t[0].lower(), t[1]) else: @@ -64,7 +65,7 @@ def patch_cache_control(response, **kwargs): if 'max-age' in cc and 'max_age' in kwargs: kwargs['max_age'] = min(cc['max-age'], kwargs['max_age']) - for (k,v) in kwargs.items(): + for (k, v) in kwargs.items(): cc[k.replace('_', '-')] = v cc = ', '.join([dictvalue(el) for el in cc.items()]) response['Cache-Control'] = cc @@ -88,15 +89,14 @@ def patch_response_headers(response, cache_timeout=None): if not response.has_header('ETag'): response['ETag'] = md5.new(response.content).hexdigest() if not response.has_header('Last-Modified'): - response['Last-Modified'] = formatdate()[:26] + "GMT" + response['Last-Modified'] = http_date() if not response.has_header('Expires'): - response['Expires'] = formatdate(time.time() + cache_timeout)[:26] + "GMT" + response['Expires'] = http_date(time.time() + cache_timeout) patch_cache_control(response, max_age=cache_timeout) def add_never_cache_headers(response): """ - Add headers to a response to indicate that - a page should never be cached. + Adds headers to a response to indicate that a page should never be cached. """ patch_response_headers(response, cache_timeout=-1) @@ -119,13 +119,14 @@ def patch_vary_headers(response, newheaders): response['Vary'] = ', '.join(vary) def _generate_cache_key(request, headerlist, key_prefix): - "Returns a cache key from the headers given in the header list." + """Returns a cache key from the headers given in the header list.""" ctx = md5.new() for header in headerlist: value = request.META.get(header, None) if value is not None: ctx.update(value) - return 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, iri_to_uri(request.path), ctx.hexdigest()) + return 'views.decorators.cache.cache_page.%s.%s.%s' % ( + key_prefix, iri_to_uri(request.path), ctx.hexdigest()) def get_cache_key(request, key_prefix=None): """ @@ -139,7 +140,8 @@ def get_cache_key(request, key_prefix=None): """ if key_prefix is None: key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX - cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, iri_to_uri(request.path)) + cache_key = 'views.decorators.cache.cache_header.%s.%s' % ( + key_prefix, iri_to_uri(request.path)) headerlist = cache.get(cache_key, None) if headerlist is not None: return _generate_cache_key(request, headerlist, key_prefix) @@ -163,9 +165,11 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None): key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX if cache_timeout is None: cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS - cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, iri_to_uri(request.path)) + cache_key = 'views.decorators.cache.cache_header.%s.%s' % ( + key_prefix, iri_to_uri(request.path)) if response.has_header('Vary'): - headerlist = ['HTTP_'+header.upper().replace('-', '_') for header in vary_delim_re.split(response['Vary'])] + headerlist = ['HTTP_'+header.upper().replace('-', '_') + for header in vary_delim_re.split(response['Vary'])] cache.set(cache_key, headerlist, cache_timeout) return _generate_cache_key(request, headerlist, key_prefix) else: diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 9a296c3f2d..bfca46b3d5 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -170,7 +170,7 @@ class DateFormat(TimeFormat): return u"%+03d%02d" % (seconds // 3600, (seconds // 60) % 60) def r(self): - "RFC 822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'" + "RFC 2822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'" return self.format('D, j M Y H:i:s O') def S(self): diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 2bd1ef6563..4bda9caa50 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -1,8 +1,19 @@ import types import urllib import datetime + from django.utils.functional import Promise +class DjangoUnicodeDecodeError(UnicodeDecodeError): + def __init__(self, obj, *args): + self.obj = obj + UnicodeDecodeError.__init__(self, *args) + + def __str__(self): + original = UnicodeDecodeError.__str__(self) + return '%s. You passed in %r (%s)' % (original, self.obj, + type(self.obj)) + class StrAndUnicode(object): """ A class whose __str__ returns its __unicode__ as a UTF-8 bytestring. @@ -33,13 +44,16 @@ def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): """ if strings_only and isinstance(s, (types.NoneType, int, long, datetime.datetime, datetime.date, datetime.time, float)): return s - if not isinstance(s, basestring,): - if hasattr(s, '__unicode__'): - s = unicode(s) - else: - s = unicode(str(s), encoding, errors) - elif not isinstance(s, unicode): - s = unicode(s, encoding, errors) + try: + if not isinstance(s, basestring,): + if hasattr(s, '__unicode__'): + s = unicode(s) + else: + s = unicode(str(s), encoding, errors) + elif not isinstance(s, unicode): + s = unicode(s, encoding, errors) + except UnicodeDecodeError, e: + raise DjangoUnicodeDecodeError(s, *e.args) return s def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): diff --git a/django/utils/http.py b/django/utils/http.py index 4912c9c46a..5ec6e92d28 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -1,4 +1,6 @@ import urllib +from email.Utils import formatdate + from django.utils.encoding import smart_str, force_unicode from django.utils.functional import allow_lazy @@ -37,3 +39,29 @@ def urlencode(query, doseq=0): for k, v in query], doseq) +def cookie_date(epoch_seconds=None): + """ + Formats the time to ensure compatibility with Netscape's cookie standard. + + Accepts a floating point number expressed in seconds since the epoch, in + UTC - such as that outputted by time.time(). If set to None, defaults to + the current time. + + Outputs a string in the format 'Wdy, DD-Mon-YYYY HH:MM:SS GMT'. + """ + rfcdate = formatdate(epoch_seconds) + return '%s-%s-%s GMT' % (rfcdate[:7], rfcdate[8:11], rfcdate[12:25]) + +def http_date(epoch_seconds=None): + """ + Formats the time to match the RFC1123 date format as specified by HTTP + RFC2616 section 3.3.1. + + Accepts a floating point number expressed in seconds since the epoch, in + UTC - such as that outputted by time.time(). If set to None, defaults to + the current time. + + Outputs a string in the format 'Wdy, DD Mon YYYY HH:MM:SS GMT'. + """ + rfcdate = formatdate(epoch_seconds) + return '%s GMT' % rfcdate[:25] diff --git a/django/views/static.py b/django/views/static.py index dce45d914d..b556c60ca6 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -7,13 +7,14 @@ import mimetypes import os import posixpath import re -import rfc822 import stat import urllib +from email.Utils import parsedate_tz, mktime_tz from django.template import loader from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified from django.template import Template, Context, TemplateDoesNotExist +from django.utils.http import http_date def serve(request, path, document_root=None, show_indexes=False): """ @@ -60,7 +61,7 @@ def serve(request, path, document_root=None, show_indexes=False): mimetype = mimetypes.guess_type(fullpath)[0] contents = open(fullpath, 'rb').read() response = HttpResponse(contents, mimetype=mimetype) - response["Last-Modified"] = rfc822.formatdate(statobj[stat.ST_MTIME]) + response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) return response DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ @@ -119,8 +120,7 @@ def was_modified_since(header=None, mtime=0, size=0): raise ValueError matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, re.IGNORECASE) - header_mtime = rfc822.mktime_tz(rfc822.parsedate_tz( - matches.group(1))) + header_mtime = mktime_tz(parsedate_tz(matches.group(1))) header_len = matches.group(3) if header_len and int(header_len) != size: raise ValueError diff --git a/docs/cache.txt b/docs/cache.txt index d598915d1a..4f177b8c07 100644 --- a/docs/cache.txt +++ b/docs/cache.txt @@ -291,13 +291,15 @@ minutes. Template fragment caching ========================= +**New in development version**. + If you're after even more control, you can also cache template fragments using -the ``cache`` template tag. To give your template access to this tag, put ``{% -load cache %}`` near the top of your template. +the ``cache`` template tag. To give your template access to this tag, put +``{% load cache %}`` near the top of your template. The ``{% cache %}`` template tag caches the contents of the block for a given -amount of time. It takes at least two arguments: the cache timeout, in -seconds, and the name to give the cache fragment. For example:: +amount of time. It takes at least two arguments: the cache timeout, in seconds, +and the name to give the cache fragment. For example:: {% load cache %} {% cache 500 sidebar %} diff --git a/docs/custom_model_fields.txt b/docs/custom_model_fields.txt new file mode 100644 index 0000000000..c12d1844cd --- /dev/null +++ b/docs/custom_model_fields.txt @@ -0,0 +1,567 @@ +=================== +Custom Model Fields +=================== + +**New in Django development version** + +Introduction +============ + +The `model reference`_ documentation explains how to use Django's standard +field classes. For many purposes, those classes are all you'll need. Sometimes, +though, the Django version won't meet your precise requirements, or you'll want +to use a field that is entirely different from those shipped with Django. + +Django's built-in field types don't cover every possible database column type -- +only the common types, such as ``VARCHAR`` and ``INTEGER``. For more obscure +column types, such as geographic polygons or even user-created types such as +`PostgreSQL custom types`_, you can define your own Django ``Field`` subclasses. + +Alternatively, you may have a complex Python object that can somehow be +serialized to fit into a standard database column type. This is another case +where a ``Field`` subclass will help you use your object with your models. + +Our example object +------------------ + +Creating custom fields requires a bit of attention to detail. To make things +easier to follow, we'll use a consistent example throughout this document. +Suppose you have a Python object representing the deal of cards in a hand of +Bridge_. It doesn't matter if you don't know how to play Bridge. You only need +to know that 52 cards are dealt out equally to four players, who are +traditionally called *north*, *east*, *south* and *west*. Our class looks +something like this:: + + class Hand(object): + def __init__(self, north, east, south, west): + # Input parameters are lists of cards ('Ah', '9s', etc) + self.north = north + self.east = east + self.south = south + self.west = west + + # ... (other possibly useful methods omitted) ... + +This is just an ordinary Python class, nothing Django-specific about it. We +would like to be able to things like this in our models (we assume the +``hand`` attribute on the model is an instance of ``Hand``):: + + + example = MyModel.objects.get(pk=1) + print example.hand.north + + new_hand = Hand(north, east, south, west) + example.hand = new_hand + example.save() + +We assign to and retrieve from the ``hand`` attribute in our model just like +any other Python class. The trick is to tell Django how to handle saving and +loading such an object + +In order to use the ``Hand`` class in our models, we **do not** have to change +this class at all. This is ideal, because it means you can easily write +model support for existing classes where you cannot change the source code. + +.. note:: + You might only be wanting to take advantage of custom database column + types and deal with the data as standard Python types in your models; + strings, or floats, for example. This case is similar to our ``Hand`` + example and we'll note any differences as we go along. + +.. _model reference: ../model_api/ +.. _PostgreSQL custom types: http://www.postgresql.org/docs/8.2/interactive/sql-createtype.html +.. _Bridge: http://en.wikipedia.org/wiki/Contract_bridge + +Background Theory +================= + +Database storage +---------------- + +The simplest way to think of a model field is that it provides a way to take a +normal Python object -- string, boolean, ``datetime``, or something more +complex like ``Hand`` -- and convert it to and from a format that is useful +when dealing with the database (and serialization, but, as we'll see later, +that falls out fairly naturally once you have the database side under control). + +Fields in a model must somehow be converted to fit into an existing database +column type. Different databases provide different sets of valid column types, +but the rule is still the same: those are the only types you have to work +with. Anything you want to store in the database must fit into one of +those types. + +Normally, you're either writing a Django field to match a particular database +column type, or there's a fairly straightforward way to convert your data to, +say, a string. + +For our ``Hand`` example, we could convert the card data to a string of 104 +characters by concatenating all the cards together in a pre-determined order. +Say, all the *north* cards first, then the *east*, *south* and *west* cards, in +that order. So ``Hand`` objects can be saved to text or character columns in +the database + +What does a field class do? +--------------------------- + +All of Django's fields (and when we say *fields* in this document, we always +mean model fields and not `form fields`_) are subclasses of +``django.db.models.Field``. Most of the information that Django records about a +field is common to all fields -- name, help text, validator lists, uniqueness +and so forth. Storing all that information is handled by ``Field``. We'll get +into the precise details of what ``Field`` can do later on; for now, suffice it +to say that everything descends from ``Field`` and then customises key pieces +of the class behaviour. + +.. _form fields: ../newforms/#fields + +It's important to realise that a Django field class is not what is stored in +your model attributes. The model attributes contain normal Python objects. The +field classes you define in a model are actually stored in the ``Meta`` class +when the model class is created (the precise details of how this is done are +unimportant here). This is because the field classes aren't necessary when +you're just creating and modifying attributes. Instead, they provide the +machinery for converting between the attribute value and what is stored in the +database or sent to the serializer. + +Keep this in mind when creating your own custom fields. The Django ``Field`` +subclass you write provides the machinery for converting between your Python +instances and the database/serializer values in various ways (there are +differences between storing a value and using a value for lookups, for +example). If this sounds a bit tricky, don't worry. It will hopefully become +clearer in the examples below. Just remember that you will often end up +creating two classes when you want a custom field. The first class is the +Python object that your users will manipulate. They will assign it to the model +attribute, they will read from it for displaying purposes, things like that. +This is the ``Hand`` class in our example. The second class is the ``Field`` +subclass. This is the class that knows how to convert your first class back and +forth between its permanent storage form and the Python form. + +Writing a ``Field`` subclass +============================= + +When you are planning your ``Field`` subclass, first give some thought to +which existing field your new field is most similar to. Can you subclass an +existing Django field and save yourself some work? If not, you should subclass the ``Field`` class, from which everything is descended. + +Initialising your new field is a matter of separating out any arguments that +are specific to your case from the common arguments and passing the latter to +the ``__init__()`` method of ``Field`` (or your parent class). + +In our example, the Django field we create is going to be called +``HandField``. It's not a bad idea to use a similar naming scheme to Django's +fields so that our new class is identifiable and yet clearly related to the +``Hand`` class it is wrapping. It doesn't behave like any existing field, so +we'll subclass directly from ``Field``:: + + from django.db import models + + class HandField(models.Field): + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 104 + super(HandField, self).__init__(*args, **kwargs) + +Our ``HandField`` will accept most of the standard field options (see the list +below), but we ensure it has a fixed length, since it only needs to hold 52 +card values plus their suits; 104 characters in total. + +.. note:: + Many of Django's model fields accept options that they don't do anything + with. For example, you can pass both ``editable`` and ``auto_now`` to a + ``DateField`` and it will simply ignore the ``editable`` parameter + (``auto_now`` being set implies ``editable=False``). No error is raised in + this case. + + This behaviour simplifies the field classes, because they don't need to + check for options that aren't necessary. They just pass all the options to + the parent class and then don't use them later on. It is up to you whether + you want your fields to be more strict about the options they select, or + to use the simpler, more permissive behaviour of the current fields. + +The ``Field.__init__()`` method takes the following parameters, in this +order: + + - ``verbose_name`` + - ``name`` + - ``primary_key`` + - ``max_length`` + - ``unique`` + - ``blank`` + - ``null`` + - ``db_index`` + - ``core`` + - ``rel``: Used for related fields (like ``ForeignKey``). For advanced use + only. + - ``default`` + - ``editable`` + - ``serialize``: If ``False``, the field will not be serialized when the + model is passed to Django's serializers_. Defaults to ``True``. + - ``prepopulate_from`` + - ``unique_for_date`` + - ``unique_for_month`` + - ``unique_for_year`` + - ``validator_list`` + - ``choices`` + - ``radio_admin`` + - ``help_text`` + - ``db_column`` + - ``db_tablespace``: Currently only used with the Oracle backend and only + for index creation. You can usually ignore this option. + +All of the options without an explanation in the above list have the same +meaning they do for normal Django fields. See the `model documentation`_ for +examples and details. + +.. _serializers: ../serialization/ +.. _model documentation: ../model-api/ + +The ``SubfieldBase`` metaclass +------------------------------ + +As we indicated in the introduction_, field subclasses are often needed for +two reasons. Either to take advantage of a custom database column type, or to +handle complex Python types. A combination of the two is obviously also +possible. If you are only working with custom database column types and your +model fields appear in Python as standard Python types direct from the +database backend, you don't need to worry about this section. + +If you are handling custom Python types, such as our ``Hand`` class, we need +to make sure that when Django initialises an instance of our model and assigns +a database value to our custom field attribute we convert that value into the +appropriate Python object. The details of how this happens internally are a +little complex. For the field writer, though, things are fairly simple. Make +sure your field subclass uses ``django.db.models.SubfieldBase`` as its +metaclass. This ensures that the ``to_python()`` method, documented below_, +will always be called when the attribute is initialised. + +Our ``HandleField`` class now looks like this:: + + class HandleField(models.Field): + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + # ... + +.. _below: #to-python-self-value + +Useful methods +-------------- + +Once you've created your ``Field`` subclass and setup up the +``__metaclass__``, if necessary, there are a few standard methods you need to +consider overriding. Which of these you need to implement will depend on you +particular field behaviour. The list below is in approximately decreasing +order of importance, so start from the top. + +``db_type(self)`` +~~~~~~~~~~~~~~~~~ + +Returns the database column data type for the ``Field``, taking into account +the current ``DATABASE_ENGINE`` setting. + +Say you've created a PostgreSQL custom type called ``mytype``. You can use this +field with Django by subclassing ``Field`` and implementing the ``db_type()`` +method, like so:: + + from django.db import models + + class MytypeField(models.Field): + def db_type(self): + return 'mytype' + +Once you have ``MytypeField``, you can use it in any model, just like any other +``Field`` type:: + + class Person(models.Model): + name = models.CharField(max_length=80) + gender = models.CharField(max_length=1) + something_else = MytypeField() + +If you aim to build a database-agnostic application, you should account for +differences in database column types. For example, the date/time column type +in PostgreSQL is called ``timestamp``, while the same column in MySQL is called +``datetime``. The simplest way to handle this in a ``db_type()`` method is to +import the Django settings module and check the ``DATABASE_ENGINE`` setting. +For example:: + + class MyDateField(models.Field): + def db_type(self): + from django.conf import settings + if settings.DATABASE_ENGINE == 'mysql': + return 'datetime' + else: + return 'timestamp' + +The ``db_type()`` method is only called by Django when the framework constructs +the ``CREATE TABLE`` statements for your application -- that is, when you first +create your tables. It's not called at any other time, so it can afford to +execute slightly complex code, such as the ``DATABASE_ENGINE`` check in the +above example. + +Some database column types accept parameters, such as ``CHAR(25)``, where the +parameter ``25`` represents the maximum column length. In cases like these, +it's more flexible if the parameter is specified in the model rather than being +hard-coded in the ``db_type()`` method. For example, it wouldn't make much +sense to have a ``CharMaxlength25Field``, shown here:: + + # This is a silly example of hard-coded parameters. + class CharMaxlength25Field(models.Field): + def db_type(self): + return 'char(25)' + + # In the model: + class MyModel(models.Model): + # ... + my_field = CharMaxlength25Field() + +The better way of doing this would be to make the parameter specifiable at run +time -- i.e., when the class is instantiated. To do that, just implement +``__init__()``, like so:: + + # This is a much more flexible example. + class BetterCharField(models.Field): + def __init__(self, max_length, *args, **kwargs): + self.max_length = max_length + super(BetterCharField, self).__init__(*args, **kwargs) + + def db_type(self): + return 'char(%s)' % self.max_length + + # In the model: + class MyModel(models.Model): + # ... + my_field = BetterCharField(25) + +Finally, if your column requires truly complex SQL setup, return ``None`` from +``db_type()``. This will cause Django's SQL creation code to skip over this +field. You are then responsible for creating the column in the right table in +some other way, of course, but this gives you a way to tell Django to get out +of the way. + + +``to_python(self, value)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Converts between all the ways your field can receive its initial value and the +Python object you want to end up with. The default version just returns +``value``, so is useful is the database backend returns the data already in +the correct form (a Python string, for example). + +Normally, you will need to override this method. As a general rule, be +prepared to accept an instance of the right type (e.g. ``Hand`` in our ongoing +example), a string (from a deserializer, for example), and whatever the +database wrapper returns for the column type you are using. + +In our ``HandField`` class, we are storing the data in a character field in +the database, so we need to be able to process strings and ``Hand`` instances +in ``to_python()``:: + + class HandField(models.Field): + # ... + + def to_python(self, value): + if isinstance(value, Hand): + return value + + # The string case + p1 = re.compile('.{26}') + p2 = re.compile('..') + args = [p2.findall(x) for x in p1.findall(value)] + return Hand(*args) + +Notice that we always return a ``Hand`` instance from this method. That is the +Python object we want to store in the model's attribute. + +``get_db_prep_save(self, value)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the reverse of ``to_python()`` when working with the database backends +(as opposed to serialization). The ``value`` parameter is the current value of +the model's attribute (a field has no reference to its containing model, so it +cannot retrieve the value itself) and the method should return data in a +format that can be used as a parameter in a query for the database backend. + +For example:: + + class HandField(models.Field): + # ... + + def get_db_prep_save(self, value): + return ''.join([''.join(l) for l in (self.north, + self.east, self.south, self.west)]) + + +``pre_save(self, model_instance, add)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This method is called just prior to ``get_db_prep_save()`` and should return +the value of the appropriate attribute from ``model_instance`` for this field. +The attribute name is in ``self.attname`` (this is set up by ``Field``). If +the model is being saved to the database for the first time, the ``add`` +parameter will be ``True``, otherwise it will be ``False``. + +Often you won't need to override this method. However, at times it can be very +useful. For example, the Django ``DateTimeField`` uses this method to set the +attribute to the correct value before returning it in the cases when +``auto_now`` or ``auto_now_add`` are set on the field. + +If you do override this method, you must return the value of the attribute at +the end. You should also update the model's attribute if you make any changes +to the value so that code holding references to the model will always see the +correct value. + +``get_db_prep_lookup(self, lookup_type, value)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Prepares the ``value`` for passing to the database when used in a lookup (a +``WHERE`` constraint in SQL). The ``lookup_type`` will be one of the valid +Django filter lookups: ``exact``, ``iexact``, ``contains``, ``icontains``, +``gt``, ``gte``, ``lt``, ``lte``, ``in``, ``startswith``, ``istartswith``, +``endswith``, ``iendswith``, ``range``, ``year``, ``month``, ``day``, +``isnull``, ``search``, ``regex``, and ``iregex``. + +Your method must be prepared to handle all of these ``lookup_type`` values and +should raise either a ``ValueError`` if the ``value`` is of the wrong sort (a +list when you were expecting an object, for example) or a ``TypeError`` if +your field does not support that type of lookup. For many fields, you can get +by with handling the lookup types that need special handling for your field +and pass the rest of the ``get_db_prep_lookup()`` method of the parent class. + +If you needed to implement ``get_db_prep_save()``, you will usually need to +implement ``get_db_prep_lookup()``. The usual reason is because of the +``range`` and ``in`` lookups. In these case, you will passed a list of +objects (presumably of the right type) and will need to convert them to a list +of things of the right type for passing to the database. Sometimes you can +reuse ``get_db_prep_save()``, or at least factor out some common pieces from +both methods into a help function. + +For example:: + + class HandField(models.Field): + # ... + + def get_db_prep_lookup(self, lookup_type, value): + # We only handle 'exact' and 'in'. All others are errors. + if lookup_type == 'exact': + return self.get_db_prep_save(value) + elif lookup_type == 'in': + return [self.get_db_prep_save(v) for v in value] + else: + raise TypeError('Lookup type %r not supported.' % lookup_type) + + +``formfield(self, form_class=forms.CharField, **kwargs)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Returns the default form field to use when this field is displayed +in a model. This method is called by the `helper functions`_ +``form_for_model()`` and ``form_for_instance()``. + +All of the ``kwargs`` dictionary is passed directly to the form field's +``__init__()`` method. Normally, all you need to do is set up a good default +for the ``form_class`` argument and then delegate further handling to the +parent class. This might require you to write a custom form field (and even a +form widget). See the `forms documentation`_ for information about this. Also +have a look at ``django.contrib.localflavor`` for some examples of custom +widgets. + +Continuing our ongoing example, we can write the ``formfield()`` method as:: + + class HandField(models.Field): + # ... + + def formfield(self, **kwargs): + # This is a fairly standard way to set up some defaults + # whilst letting the caller override them. + defaults = {'form_class': MyFormField} + defaults.update(kwargs) + return super(HandField, self).formfield(**defaults) + +This assumes we have some ``MyFormField`` field class (which has its own +default widget) imported. This document doesn't cover the details of writing +custom form fields. + +.. _helper functions: ../newforms/#generating-forms-for-models +.. _forms documentation: ../newforms/ + +``get_internal_type(self)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Returns a string giving the name of the ``Field`` subclass we are emulating at +the database level. This is used to determine the type of database column for +simple cases. + +If you have created a ``db_type()`` method, you do not need to worry about +``get_internal_type()`` -- it won't be used much. Sometimes, though, your +database storage is similar in type to some other field, so you can use that +other field's logic to create the right column. + +For example:: + + class HandField(models.Field): + # ... + + def get_internal_type(self): + return 'CharField' + +No matter which database backend we are using, this will mean that ``syncdb`` +and other SQL commands create the right column type for storing a string. + +If ``get_internal_type()`` returns a string that is not known to Django for +the database backend you are using -- that is, it doesn't appear in +``django.db.backends..creation.DATA_TYPES`` -- the string will still +be used by the serializer, but the default ``db_type()`` method will return +``None``. See the documentation of ``db_type()`` above_ for reasons why this +might be useful. Putting a descriptive string in as the type of the field for +the serializer is a useful idea if you are ever going to be using the +serializer output in some other place, outside of Django. + +.. _above: #db-type-self + +``flatten_data(self, follow, obj=None)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. admonition:: Subject to change + + Although implementing this method is necessary to allow field + serialization, the API might change in the future. + +Returns a dictionary, mapping the field's attribute name to a flattened string +version of the data. This method has some internal uses that aren't of +interest to use here (mostly having to do with manipulators). For our +purposes, it is sufficient to return a one item dictionary that maps the +attribute name to a string. + +This method is used by the serializers to convert the field into a string for +output. You can ignore the input parameters for serialization purposes, +although calling ``Field._get_val_from_obj(obj)`` is the best way to get the +value to serialize. + +For example, since our ``HandField`` uses strings for its data storage anyway, +we can reuse some existing conversion code:: + + class HandField(models.Field): + # ... + + def flatten_data(self, follow, obj=None): + value = self._get_val_from_obj(obj) + return {self.attname: self.get_db_prep_save(value)} + +Some general advice +-------------------- + +Writing a custom field can be a tricky process sometime, particularly if you +are doing complex conversions between your Python types and your database and +serialization formats. A couple of tips to make things go more smoothly: + + 1. Look at the existing Django fields (in + ``django/db/models/fields/__init__.py``) for inspiration. Try to find a field + that is already close to what you want and extend it a little bit, in + preference to creating an entirely new field from scratch. + + 2. Put a ``__str__()`` or ``__unicode__()`` method on the class you are + wrapping up as a field. There are a lot of places where the default behaviour + of the field code is to call ``force_unicode()`` on the value (in our + examples in this document, ``value`` would be a ``Hand`` instance, not a + ``HandField``). So if your ``__unicode__()`` method automatically converts to + the string form of your Python object, you can save yourself a lot of work. + diff --git a/docs/email.txt b/docs/email.txt index effc5e24cf..55b91b1935 100644 --- a/docs/email.txt +++ b/docs/email.txt @@ -275,7 +275,7 @@ The class has the following methods: There are two ways to call ``attach()``: * You can pass it a single argument that is an - ``email.MIMBase.MIMEBase`` instance. This will be inserted directly + ``email.MIMEBase.MIMEBase`` instance. This will be inserted directly into the resulting message. * Alternatively, you can pass ``attach()`` three arguments: diff --git a/docs/form_preview.txt b/docs/form_preview.txt index 4be7b07a74..e6f9b05f25 100644 --- a/docs/form_preview.txt +++ b/docs/form_preview.txt @@ -45,7 +45,7 @@ How to use ``FormPreview`` 2. Create a ``FormPreview`` subclass that overrides the ``done()`` method:: - from django.contrib.formtools import FormPreview + from django.contrib.formtools.preview import FormPreview from myapp.models import SomeModel class SomeModelFormPreview(FormPreview): diff --git a/docs/model-api.txt b/docs/model-api.txt index b49963d8f5..ca84c84d09 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -1013,111 +1013,12 @@ See the `One-to-one relationship model example`_ for a full example. Custom field types ------------------ -**New in Django development version** +If one of the existing model fields cannot be used to fit your purposes, or if +you wish to take advantage of some less common database column types, you can +create your own field class. Full coverage of creating your own fields is +provided in the `Custom Model Fields`_ documentation. -Django's built-in field types don't cover every possible database column type -- -only the common types, such as ``VARCHAR`` and ``INTEGER``. For more obscure -column types, such as geographic polygons or even user-created types such as -`PostgreSQL custom types`_, you can define your own Django ``Field`` subclasses. - -.. _PostgreSQL custom types: http://www.postgresql.org/docs/8.2/interactive/sql-createtype.html - -.. admonition:: Experimental territory - - This is an area of Django that traditionally has not been documented, but - we're starting to include bits of documentation, one feature at a time. - Please forgive the sparseness of this section. - - If you like living on the edge and are comfortable with the risk of - unstable, undocumented APIs, see the code for the core ``Field`` class - in ``django/db/models/fields/__init__.py`` -- but if/when the innards - change, don't say we didn't warn you. - -To create a custom field type, simply subclass ``django.db.models.Field``. -Here is an incomplete list of the methods you should implement: - -``db_type()`` -~~~~~~~~~~~~~ - -Returns the database column data type for the ``Field``, taking into account -the current ``DATABASE_ENGINE`` setting. - -Say you've created a PostgreSQL custom type called ``mytype``. You can use this -field with Django by subclassing ``Field`` and implementing the ``db_type()`` -method, like so:: - - from django.db import models - - class MytypeField(models.Field): - def db_type(self): - return 'mytype' - -Once you have ``MytypeField``, you can use it in any model, just like any other -``Field`` type:: - - class Person(models.Model): - name = models.CharField(max_length=80) - gender = models.CharField(max_length=1) - something_else = MytypeField() - -If you aim to build a database-agnostic application, you should account for -differences in database column types. For example, the date/time column type -in PostgreSQL is called ``timestamp``, while the same column in MySQL is called -``datetime``. The simplest way to handle this in a ``db_type()`` method is to -import the Django settings module and check the ``DATABASE_ENGINE`` setting. -For example:: - - class MyDateField(models.Field): - def db_type(self): - from django.conf import settings - if settings.DATABASE_ENGINE == 'mysql': - return 'datetime' - else: - return 'timestamp' - -The ``db_type()`` method is only called by Django when the framework constructs -the ``CREATE TABLE`` statements for your application -- that is, when you first -create your tables. It's not called at any other time, so it can afford to -execute slightly complex code, such as the ``DATABASE_ENGINE`` check in the -above example. - -Some database column types accept parameters, such as ``CHAR(25)``, where the -parameter ``25`` represents the maximum column length. In cases like these, -it's more flexible if the parameter is specified in the model rather than being -hard-coded in the ``db_type()`` method. For example, it wouldn't make much -sense to have a ``CharMaxlength25Field``, shown here:: - - # This is a silly example of hard-coded parameters. - class CharMaxlength25Field(models.Field): - def db_type(self): - return 'char(25)' - - # In the model: - class MyModel(models.Model): - # ... - my_field = CharMaxlength25Field() - -The better way of doing this would be to make the parameter specifiable at run -time -- i.e., when the class is instantiated. To do that, just implement -``__init__()``, like so:: - - # This is a much more flexible example. - class BetterCharField(models.Field): - def __init__(self, max_length, *args, **kwargs): - self.max_length = max_length - super(BetterCharField, self).__init__(*args, **kwargs) - - def db_type(self): - return 'char(%s)' % self.max_length - - # In the model: - class MyModel(models.Model): - # ... - my_field = BetterCharField(25) - -Note that if you implement ``__init__()`` on a ``Field`` subclass, it's -important to call ``Field.__init__()`` -- i.e., the parent class' -``__init__()`` method. +.. _Custom Model Fields: ../custom_model_fields/ Meta options ============ diff --git a/docs/modpython.txt b/docs/modpython.txt index 5b20046168..c739997ce4 100644 --- a/docs/modpython.txt +++ b/docs/modpython.txt @@ -150,7 +150,7 @@ mess things up. Use the ``PythonInterpreter`` directive to give different SetEnv DJANGO_SETTINGS_MODULE mysite.other_settings - PythonInterpreter mysite_other + PythonInterpreter othersite diff --git a/docs/newforms.txt b/docs/newforms.txt index a04910d2af..fe7106c26f 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -1081,6 +1081,30 @@ fields. We've specified ``auto_id=False`` to simplify the output::

Sender: A valid e-mail address, please.

Cc myself:

+``error_messages`` +~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +The ``error_messages`` argument lets you override the default messages which the +field will raise. Pass in a dictionary with keys matching the error messages you +want to override. For example:: + + >>> generic = forms.CharField() + >>> generic.clean('') + Traceback (most recent call last): + ... + ValidationError: [u'This field is required.'] + + >>> name = forms.CharField(error_messages={'required': 'Please enter your name'}) + >>> name.clean('') + Traceback (most recent call last): + ... + ValidationError: [u'Please enter your name'] + +In the `built-in Field classes`_ section below, each Field defines the error +message keys it uses. + Dynamic initial values ---------------------- @@ -1146,6 +1170,7 @@ For each field, we describe the default widget used if you don't specify * Normalizes to: A Python ``True`` or ``False`` value. * Validates that the check box is checked (i.e. the value is ``True``) if the field has ``required=True``. + * Error message keys: ``required`` **New in Django development version:** The empty value for a ``CheckboxInput`` (and hence the standard ``BooleanField``) has changed to return ``False`` @@ -1165,6 +1190,7 @@ instead of ``None`` in the development version. * Normalizes to: A Unicode object. * Validates ``max_length`` or ``min_length``, if they are provided. Otherwise, all inputs are valid. + * Error message keys: ``required``, ``max_length``, ``min_length`` Has two optional arguments for validation, ``max_length`` and ``min_length``. If provided, these arguments ensure that the string is at most or at least the @@ -1177,6 +1203,7 @@ given length. * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. * Validates that the given value exists in the list of choices. + * Error message keys: ``required``, ``invalid_choice`` Takes one extra argument, ``choices``, which is an iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field. @@ -1189,6 +1216,7 @@ tuple) of 2-tuples to use as choices for this field. * Normalizes to: A Python ``datetime.date`` object. * Validates that the given value is either a ``datetime.date``, ``datetime.datetime`` or string formatted in a particular date format. + * Error message keys: ``required``, ``invalid`` Takes one optional argument, ``input_formats``, which is a list of formats used to attempt to convert a string to a valid ``datetime.date`` object. @@ -1209,6 +1237,7 @@ If no ``input_formats`` argument is provided, the default input formats are:: * Normalizes to: A Python ``datetime.datetime`` object. * Validates that the given value is either a ``datetime.datetime``, ``datetime.date`` or string formatted in a particular datetime format. + * Error message keys: ``required``, ``invalid`` Takes one optional argument, ``input_formats``, which is a list of formats used to attempt to convert a string to a valid ``datetime.datetime`` object. @@ -1238,6 +1267,9 @@ If no ``input_formats`` argument is provided, the default input formats are:: * Normalizes to: A Python ``decimal``. * Validates that the given value is a decimal. Leading and trailing whitespace is ignored. + * Error message keys: ``required``, ``invalid``, ``max_value``, + ``min_value``, ``max_digits``, ``max_decimal_places``, + ``max_whole_digits`` Takes four optional arguments: ``max_value``, ``min_value``, ``max_digits``, and ``decimal_places``. The first two define the limits for the fields value. @@ -1254,6 +1286,7 @@ decimal places permitted. * Normalizes to: A Unicode object. * Validates that the given value is a valid e-mail address, using a moderately complex regular expression. + * Error message keys: ``required``, ``invalid`` Has two optional arguments for validation, ``max_length`` and ``min_length``. If provided, these arguments ensure that the string is at most or at least the @@ -1269,6 +1302,7 @@ given length. * Normalizes to: An ``UploadedFile`` object that wraps the file content and file name into a single object. * Validates that non-empty file data has been bound to the form. + * Error message keys: ``required``, ``invalid``, ``missing``, ``empty`` An ``UploadedFile`` object has two attributes: @@ -1299,6 +1333,8 @@ When you use a ``FileField`` on a form, you must also remember to and file name into a single object. * Validates that file data has been bound to the form, and that the file is of an image format understood by PIL. + * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``, + ``invalid_image`` Using an ImageField requires that the `Python Imaging Library`_ is installed. @@ -1315,6 +1351,8 @@ When you use a ``FileField`` on a form, you must also remember to * Normalizes to: A Python integer or long integer. * Validates that the given value is an integer. Leading and trailing whitespace is allowed, as in Python's ``int()`` function. + * Error message keys: ``required``, ``invalid``, ``max_value``, + ``min_value`` Takes two optional arguments for validation, ``max_value`` and ``min_value``. These control the range of values permitted in the field. @@ -1327,6 +1365,7 @@ These control the range of values permitted in the field. * Normalizes to: A Unicode object. * Validates that the given value is a valid IPv4 address, using a regular expression. + * Error message keys: ``required``, ``invalid`` ``MultipleChoiceField`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1336,6 +1375,7 @@ These control the range of values permitted in the field. * Normalizes to: A list of Unicode objects. * Validates that every value in the given list of values exists in the list of choices. + * Error message keys: ``required``, ``invalid_choice``, ``invalid_list`` Takes one extra argument, ``choices``, which is an iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field. @@ -1356,6 +1396,7 @@ tuple) of 2-tuples to use as choices for this field. * Normalizes to: A Unicode object. * Validates that the given value matches against a certain regular expression. + * Error message keys: ``required``, ``invalid`` Takes one required argument, ``regex``, which is a regular expression specified either as a string or a compiled regular expression object. @@ -1367,11 +1408,13 @@ Also takes the following optional arguments: ====================== ===================================================== ``max_length`` Ensures the string has at most this many characters. ``min_length`` Ensures the string has at least this many characters. - ``error_message`` Error message to return for failed validation. If no - message is provided, a generic error message will be - used. ====================== ===================================================== +The optional argument ``error_message`` is also accepted for backwards +compatibility. The preferred way to provide an error message is to use the +``error_messages`` argument, passing a dictionary with ``'invalid'`` as a key +and the error message as the value. + ``TimeField`` ~~~~~~~~~~~~~ @@ -1380,6 +1423,7 @@ Also takes the following optional arguments: * Normalizes to: A Python ``datetime.time`` object. * Validates that the given value is either a ``datetime.time`` or string formatted in a particular time format. + * Error message keys: ``required``, ``invalid`` Takes one optional argument, ``input_formats``, which is a list of formats used to attempt to convert a string to a valid ``datetime.time`` object. @@ -1396,6 +1440,7 @@ If no ``input_formats`` argument is provided, the default input formats are:: * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. * Validates that the given value is a valid URL. + * Error message keys: ``required``, ``invalid``, ``invalid_link`` Takes the following optional arguments: diff --git a/docs/serialization.txt b/docs/serialization.txt index fa9b4edd51..cf8b196931 100644 --- a/docs/serialization.txt +++ b/docs/serialization.txt @@ -47,14 +47,14 @@ This is useful if you want to serialize data directly to a file-like object Subset of fields ~~~~~~~~~~~~~~~~ -If you only want a subset of fields to be serialized, you can +If you only want a subset of fields to be serialized, you can specify a ``fields`` argument to the serializer:: from django.core import serializers data = serializers.serialize('xml', SomeModel.objects.all(), fields=('name','size')) In this example, only the ``name`` and ``size`` attributes of each model will -be serialized. +be serialized. .. note:: @@ -111,9 +111,9 @@ Django "ships" with a few included serializers: ``python`` Translates to and from "simple" Python objects (lists, dicts, strings, etc.). Not really all that useful on its own, but used as a base for other serializers. - - ``yaml`` Serializes to YAML (Yet Another Markup Lanuage). This - serializer is only available if PyYAML_ is installed. + + ``yaml`` Serializes to YAML (Yet Another Markup Lanuage). This + serializer is only available if PyYAML_ is installed. ========== ============================================================== .. _json: http://json.org/ @@ -135,6 +135,23 @@ For example:: json_serializer = serializers.get_serializer("json")() json_serializer.serialize(queryset, ensure_ascii=False, stream=response) +Django ships with a copy of simplejson_ in the source. Be aware, that if +you're using that for serializing directly that not all Django output can be +passed unmodified to simplejson. In particular, `lazy translation objects`_ +need a `special encoder`_ written for them. Something like this will work:: + + from django.utils.functional import Promise + from django.utils.encoding import force_unicode + + class LazyEncoder(simplejson.JSONEncoder): + def default(self, obj): + if isinstance(obj, Promise): + return force_unicode(obj) + return obj + +.. _lazy translation objects: ../i18n/#lazy-translation +.. _special encoder: http://svn.red-bean.com/bob/simplejson/tags/simplejson-1.7/docs/index.html + Writing custom serializers `````````````````````````` diff --git a/docs/templates.txt b/docs/templates.txt index 5d5f657747..68dbfa3e63 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -740,7 +740,7 @@ Available format strings: if they're zero and the special-case strings 'midnight' and 'noon' if appropriate. Proprietary extension. - r RFC 822 formatted date. ``'Thu, 21 Dec 2000 16:01:07 +0200'`` + r RFC 2822 formatted date. ``'Thu, 21 Dec 2000 16:01:07 +0200'`` s Seconds, 2 digits with leading zeros. ``'00'`` to ``'59'`` S English ordinal suffix for day of the ``'st'``, ``'nd'``, ``'rd'`` or ``'th'`` month, 2 characters. @@ -1106,25 +1106,39 @@ floatformat When used without an argument, rounds a floating-point number to one decimal place -- but only if there's a decimal part to be displayed. For example: - * ``36.123`` gets converted to ``36.1`` - * ``36.15`` gets converted to ``36.2`` - * ``36`` gets converted to ``36`` +======== ======================= ====== +value Template Output +======== ======================= ====== +34.23234 {{ value|floatformat }} 34.2 +34.00000 {{ value|floatformat }} 34 +34.26000 {{ value|floatformat }} 34.3 +======== ======================= ====== -If used with a numeric integer argument, ``floatformat`` rounds a number to that -many decimal places. For example: +If used with a numeric integer argument, ``floatformat`` rounds a number to +that many decimal places. For example: - * ``36.1234`` with floatformat:3 gets converted to ``36.123`` - * ``36`` with floatformat:4 gets converted to ``36.0000`` +======== ========================= ====== +value Template Output +======== ========================= ====== +34.23234 {{ value|floatformat:3 }} 34.232 +34.00000 {{ value|floatformat:3 }} 34.000 +34.26000 {{ value|floatformat:3 }} 34.260 +======== ========================= ====== -If the argument passed to ``floatformat`` is negative, it will round a number to -that many decimal places -- but only if there's a decimal part to be displayed. -For example: +If the argument passed to ``floatformat`` is negative, it will round a number +to that many decimal places -- but only if there's a decimal part to be +displayed. For example: - * ``36.1234`` with floatformat:-3 gets converted to ``36.123`` - * ``36`` with floatformat:-4 gets converted to ``36`` +======== ============================ ====== +value Template Output +======== ============================ ====== +34.23234 {{ value|floatformat:"-3" }} 34.232 +34.00000 {{ value|floatformat:"-3" }} 34 +34.26000 {{ value|floatformat:"-3" }} 34.260 +======== ============================ ====== -Using ``floatformat`` with no argument is equivalent to using ``floatformat`` with -an argument of ``-1``. +Using ``floatformat`` with no argument is equivalent to using ``floatformat`` +with an argument of ``-1``. get_digit ~~~~~~~~~ diff --git a/docs/testing.txt b/docs/testing.txt index 04c999cda8..7705380eff 100644 --- a/docs/testing.txt +++ b/docs/testing.txt @@ -721,7 +721,6 @@ This means, instead of instantiating a ``Client`` in each test:: ...you can just refer to ``self.client``, like so:: from django.test import TestCase - from django.test.client import Client class SimpleTest(TestCase): def test_details(self): diff --git a/tests/modeltests/field_subclassing/__init__.py b/tests/modeltests/field_subclassing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/field_subclassing/models.py b/tests/modeltests/field_subclassing/models.py new file mode 100644 index 0000000000..6182266c22 --- /dev/null +++ b/tests/modeltests/field_subclassing/models.py @@ -0,0 +1,106 @@ +""" +Tests for field subclassing. +""" + +from django.db import models +from django.utils.encoding import force_unicode +from django.core import serializers + +class Small(object): + """ + A simple class to show that non-trivial Python objects can be used as + attributes. + """ + def __init__(self, first, second): + self.first, self.second = first, second + + def __unicode__(self): + return u'%s%s' % (force_unicode(self.first), force_unicode(self.second)) + + def __str__(self): + return unicode(self).encode('utf-8') + +class SmallField(models.Field): + """ + Turns the "Small" class into a Django field. Because of the similarities + with normal character fields and the fact that Small.__unicode__ does + something sensible, we don't need to implement a lot here. + """ + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 2 + super(SmallField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return 'CharField' + + def to_python(self, value): + if isinstance(value, Small): + return value + return Small(value[0], value[1]) + + def get_db_prep_save(self, value): + return unicode(value) + + def get_db_prep_lookup(self, lookup_type, value): + if lookup_type == 'exact': + return force_unicode(value) + if lookup_type == 'in': + return [force_unicode(v) for v in value] + if lookup_type == 'isnull': + return [] + raise TypeError('Invalid lookup type: %r' % lookup_type) + + def flatten_data(self, follow, obj=None): + return {self.attname: force_unicode(self._get_val_from_obj(obj))} + +class MyModel(models.Model): + name = models.CharField(max_length=10) + data = SmallField('small field') + + def __unicode__(self): + return force_unicode(self.name) + +__test__ = {'API_TESTS': ur""" +# Creating a model with custom fields is done as per normal. +>>> s = Small(1, 2) +>>> print s +12 +>>> m = MyModel(name='m', data=s) +>>> m.save() + +# Custom fields still have normal field's attributes. +>>> m._meta.get_field('data').verbose_name +'small field' + +# The m.data attribute has been initialised correctly. It's a Small object. +>>> m.data.first, m.data.second +(1, 2) + +# The data loads back from the database correctly and 'data' has the right type. +>>> m1 = MyModel.objects.get(pk=m.pk) +>>> isinstance(m1.data, Small) +True +>>> print m1.data +12 + +# We can do normal filtering on the custom field (and will get an error when we +# use a lookup type that does not make sense). +>>> s1 = Small(1, 3) +>>> s2 = Small('a', 'b') +>>> MyModel.objects.filter(data__in=[s, s1, s2]) +[] +>>> MyModel.objects.filter(data__lt=s) +Traceback (most recent call last): +... +TypeError: Invalid lookup type: 'lt' + +# Serialization works, too. +>>> stream = serializers.serialize("json", MyModel.objects.all()) +>>> stream +'[{"pk": 1, "model": "field_subclassing.mymodel", "fields": {"data": "12", "name": "m"}}]' +>>> obj = list(serializers.deserialize("json", stream))[0] +>>> obj.object == m +True +"""} diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py index 9482f1cc9f..270642d4a0 100644 --- a/tests/regressiontests/defaultfilters/tests.py +++ b/tests/regressiontests/defaultfilters/tests.py @@ -17,6 +17,10 @@ u'0' u'7.700' >>> floatformat(6.000000,3) u'6.000' +>>> floatformat(6.200000, 3) +u'6.200' +>>> floatformat(6.200000, -3) +u'6.200' >>> floatformat(13.1031,-3) u'13.103' >>> floatformat(11.1197, -2) diff --git a/tests/regressiontests/forms/error_messages.py b/tests/regressiontests/forms/error_messages.py new file mode 100644 index 0000000000..ff7e110f6f --- /dev/null +++ b/tests/regressiontests/forms/error_messages.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +tests = r""" +>>> from django.newforms import * + +# CharField ################################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' +>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' +>>> f = CharField(min_length=5, max_length=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('1234') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 4, MIN LENGTH 5'] +>>> f.clean('12345678901') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 11, MAX LENGTH 10'] + +# IntegerField ################################################################ + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_value'] = 'MIN VALUE IS %s' +>>> e['max_value'] = 'MAX VALUE IS %s' +>>> f = IntegerField(min_value=5, max_value=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('4') +Traceback (most recent call last): +... +ValidationError: [u'MIN VALUE IS 5'] +>>> f.clean('11') +Traceback (most recent call last): +... +ValidationError: [u'MAX VALUE IS 10'] + +# FloatField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_value'] = 'MIN VALUE IS %s' +>>> e['max_value'] = 'MAX VALUE IS %s' +>>> f = FloatField(min_value=5, max_value=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('4') +Traceback (most recent call last): +... +ValidationError: [u'MIN VALUE IS 5'] +>>> f.clean('11') +Traceback (most recent call last): +... +ValidationError: [u'MAX VALUE IS 10'] + +# DecimalField ################################################################ + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_value'] = 'MIN VALUE IS %s' +>>> e['max_value'] = 'MAX VALUE IS %s' +>>> e['max_digits'] = 'MAX DIGITS IS %s' +>>> e['max_decimal_places'] = 'MAX DP IS %s' +>>> e['max_whole_digits'] = 'MAX DIGITS BEFORE DP IS %s' +>>> f = DecimalField(min_value=5, max_value=10, error_messages=e) +>>> f2 = DecimalField(max_digits=4, decimal_places=2, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('4') +Traceback (most recent call last): +... +ValidationError: [u'MIN VALUE IS 5'] +>>> f.clean('11') +Traceback (most recent call last): +... +ValidationError: [u'MAX VALUE IS 10'] +>>> f2.clean('123.45') +Traceback (most recent call last): +... +ValidationError: [u'MAX DIGITS IS 4'] +>>> f2.clean('1.234') +Traceback (most recent call last): +... +ValidationError: [u'MAX DP IS 2'] +>>> f2.clean('123.4') +Traceback (most recent call last): +... +ValidationError: [u'MAX DIGITS BEFORE DP IS 2'] + +# DateField ################################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> f = DateField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] + +# TimeField ################################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> f = TimeField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] + +# DateTimeField ############################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> f = DateTimeField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] + +# RegexField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' +>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' +>>> f = RegexField(r'^\d+$', min_length=5, max_length=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abcde') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('1234') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 4, MIN LENGTH 5'] +>>> f.clean('12345678901') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 11, MAX LENGTH 10'] + +# EmailField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' +>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' +>>> f = EmailField(min_length=8, max_length=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abcdefgh') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('a@b.com') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 7, MIN LENGTH 8'] +>>> f.clean('aye@bee.com') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 11, MAX LENGTH 10'] + +# FileField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['missing'] = 'MISSING' +>>> e['empty'] = 'EMPTY FILE' +>>> f = FileField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean({}) +Traceback (most recent call last): +... +ValidationError: [u'MISSING'] +>>> f.clean({'filename': 'name', 'content':''}) +Traceback (most recent call last): +... +ValidationError: [u'EMPTY FILE'] + +# URLField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['invalid_link'] = 'INVALID LINK' +>>> f = URLField(verify_exists=True, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc.c') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') +Traceback (most recent call last): +... +ValidationError: [u'INVALID LINK'] + +# BooleanField ################################################################ + +>>> e = {'required': 'REQUIRED'} +>>> f = BooleanField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] + +# ChoiceField ################################################################# + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid_choice'] = '%(value)s IS INVALID CHOICE' +>>> f = ChoiceField(choices=[('a', 'aye')], error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('b') +Traceback (most recent call last): +... +ValidationError: [u'b IS INVALID CHOICE'] + +# MultipleChoiceField ######################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid_choice'] = '%(value)s IS INVALID CHOICE' +>>> e['invalid_list'] = 'NOT A LIST' +>>> f = MultipleChoiceField(choices=[('a', 'aye')], error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('b') +Traceback (most recent call last): +... +ValidationError: [u'NOT A LIST'] +>>> f.clean(['b']) +Traceback (most recent call last): +... +ValidationError: [u'b IS INVALID CHOICE'] + +# SplitDateTimeField ########################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid_date'] = 'INVALID DATE' +>>> e['invalid_time'] = 'INVALID TIME' +>>> f = SplitDateTimeField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean(['a', 'b']) +Traceback (most recent call last): +... +ValidationError: [u'INVALID DATE', u'INVALID TIME'] + +# IPAddressField ############################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID IP ADDRESS' +>>> f = IPAddressField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('127.0.0') +Traceback (most recent call last): +... +ValidationError: [u'INVALID IP ADDRESS'] +""" diff --git a/tests/regressiontests/forms/extra.py b/tests/regressiontests/forms/extra.py index 7f6175f649..9dff4071f1 100644 --- a/tests/regressiontests/forms/extra.py +++ b/tests/regressiontests/forms/extra.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- tests = r""" >>> from django.newforms import * +>>> from django.utils.encoding import force_unicode >>> import datetime >>> import time >>> import re @@ -362,7 +363,7 @@ u'sirrobin' ... return self.as_divs() ... def as_divs(self): ... if not self: return u'' -... return u'
%s
' % ''.join([u'
%s
' % e for e in self]) +... return u'
%s
' % ''.join([u'
%s
' % force_unicode(e) for e in self]) >>> class CommentForm(Form): ... name = CharField(max_length=50, required=False) ... email = EmailField() diff --git a/tests/regressiontests/forms/regressions.py b/tests/regressiontests/forms/regressions.py index 1bfb425188..1bb6f6e7e5 100644 --- a/tests/regressiontests/forms/regressions.py +++ b/tests/regressiontests/forms/regressions.py @@ -26,7 +26,6 @@ There were some problems with form translations in #3600 Translations are done at rendering time, so multi-lingual apps can define forms early and still send back the right translation. -# XFAIL >>> activate('de') >>> print f.as_p()

diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index e90d6d2510..333b928700 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -2,6 +2,7 @@ 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 from localflavor.au import tests as localflavor_au_tests from localflavor.br import tests as localflavor_br_tests @@ -31,6 +32,7 @@ __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, 'localflavor_au_tests': localflavor_au_tests, 'localflavor_br_tests': localflavor_br_tests, diff --git a/tests/regressiontests/forms/util.py b/tests/regressiontests/forms/util.py index 4f81709082..bfaf73f6bc 100644 --- a/tests/regressiontests/forms/util.py +++ b/tests/regressiontests/forms/util.py @@ -42,4 +42,11 @@ u'' # Can take a mixture in a list. >>> print ValidationError(["First error.", u"Not \u03C0.", ugettext_lazy("Error.")]).messages
  • First error.
  • Not π.
  • Error.
+ +>>> class VeryBadError: +... def __unicode__(self): return u"A very bad error." + +# Can take a non-string. +>>> print ValidationError(VeryBadError()).messages +
  • A very bad error.
""" diff --git a/tests/regressiontests/templates/urls.py b/tests/regressiontests/templates/urls.py index d79f38e0a7..3b28e70d0b 100644 --- a/tests/regressiontests/templates/urls.py +++ b/tests/regressiontests/templates/urls.py @@ -7,7 +7,7 @@ urlpatterns = patterns('', # Test urls for testing reverse lookups (r'^$', views.index), (r'^client/(\d+)/$', views.client), - (r'^client/(\d+)/(?P[^/]+)/$', views.client_action), + (r'^client/(?P\d+)/(?P[^/]+)/$', views.client_action), url(r'^named-client/(\d+)/$', views.client, name="named.client"), # Unicode strings are permitted everywhere. diff --git a/tests/regressiontests/text/tests.py b/tests/regressiontests/text/tests.py index 962a30ef19..7cfe44517a 100644 --- a/tests/regressiontests/text/tests.py +++ b/tests/regressiontests/text/tests.py @@ -27,6 +27,14 @@ u'Paris+%26+Orl%C3%A9ans' >>> urlquote_plus(u'Paris & Orl\xe9ans', safe="&") u'Paris+&+Orl%C3%A9ans' +### cookie_date, http_date ############################################### +>>> from django.utils.http import cookie_date, http_date +>>> t = 1167616461.0 +>>> cookie_date(t) +'Mon, 01-Jan-2007 01:54:21 GMT' +>>> http_date(t) +'Mon, 01 Jan 2007 01:54:21 GMT' + ### iri_to_uri ########################################################### >>> from django.utils.encoding import iri_to_uri >>> iri_to_uri(u'red%09ros\xe9#red')