From 94c320d8a982ce30f6dd4e7556f2696229b761c7 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 20 Sep 2007 01:55:53 +0000 Subject: [PATCH] queryset-refactor: Merged to [6381] git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6382 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 6 + django/bin/compile-messages.py | 35 +- django/contrib/admin/views/main.py | 2 +- django/contrib/auth/backends.py | 44 + django/contrib/auth/models.py | 90 +- django/contrib/sessions/backends/base.py | 2 +- django/contrib/sessions/backends/file.py | 13 +- django/core/context_processors.py | 14 +- django/core/handlers/modpython.py | 7 +- django/db/backends/ado_mssql/creation.py | 6 +- django/db/backends/mysql/creation.py | 6 +- django/db/backends/mysql_old/creation.py | 6 +- django/db/backends/oracle/creation.py | 8 +- django/db/backends/postgresql/creation.py | 6 +- django/db/backends/sqlite3/creation.py | 6 +- django/db/models/base.py | 5 + django/db/models/fields/__init__.py | 10 +- django/middleware/http.py | 5 +- django/newforms/fields.py | 10 +- django/newforms/forms.py | 10 +- django/oldforms/__init__.py | 6 +- django/utils/datastructures.py | 20 + django/utils/dateformat.py | 4 +- django/utils/timesince.py | 18 +- docs/apache_auth.txt | 4 +- docs/authentication.txt | 37 + docs/django-admin.txt | 6 +- docs/email.txt | 2 +- docs/i18n.txt | 9 +- docs/install.txt | 2 +- docs/model-api.txt | 24 + docs/newforms.txt | 31 +- docs/sessions.txt | 35 +- docs/settings.txt | 7 +- docs/templates.txt | 10 +- tests/modeltests/basic/models.py | 5 + tests/modeltests/custom_pk/models.py | 9 + tests/modeltests/fixtures/models.py | 9 +- .../regressiontests/auth_backends/__init__.py | 0 tests/regressiontests/auth_backends/models.py | 0 tests/regressiontests/auth_backends/tests.py | 66 + tests/regressiontests/backends/models.py | 16 +- tests/regressiontests/forms/extra.py | 398 ++ tests/regressiontests/forms/fields.py | 1162 +++++ tests/regressiontests/forms/forms.py | 1606 +++++++ tests/regressiontests/forms/localflavor.py | 2197 --------- .../forms/localflavor/__init__.py | 1 + tests/regressiontests/forms/localflavor/ar.py | 294 ++ tests/regressiontests/forms/localflavor/au.py | 134 + tests/regressiontests/forms/localflavor/br.py | 220 + tests/regressiontests/forms/localflavor/ca.py | 221 + tests/regressiontests/forms/localflavor/ch.py | 58 + tests/regressiontests/forms/localflavor/cl.py | 74 + tests/regressiontests/forms/localflavor/de.py | 35 + tests/regressiontests/forms/localflavor/fi.py | 99 + tests/regressiontests/forms/localflavor/fr.py | 224 + .../forms/localflavor/generic.py | 163 + .../regressiontests/forms/localflavor/is_.py | 103 + tests/regressiontests/forms/localflavor/it.py | 62 + tests/regressiontests/forms/localflavor/jp.py | 106 + tests/regressiontests/forms/localflavor/nl.py | 72 + tests/regressiontests/forms/localflavor/pl.py | 61 + tests/regressiontests/forms/localflavor/sk.py | 31 + tests/regressiontests/forms/localflavor/uk.py | 48 + tests/regressiontests/forms/localflavor/us.py | 260 ++ tests/regressiontests/forms/regressions.py | 2 +- tests/regressiontests/forms/tests.py | 3939 +---------------- tests/regressiontests/forms/util.py | 2 +- tests/regressiontests/forms/widgets.py | 848 ++++ tests/regressiontests/max_lengths/__init__.py | 1 + tests/regressiontests/max_lengths/models.py | 13 + tests/regressiontests/max_lengths/tests.py | 36 + tests/regressiontests/templates/tests.py | 8 + tests/regressiontests/utils/tests.py | 10 + tests/regressiontests/utils/timesince.py | 77 + tests/regressiontests/views/__init__.py | 0 .../views/fixtures/testdata.json | 48 + .../views/locale/en/LC_MESSAGES/djangojs.mo | Bin 0 -> 452 bytes .../views/locale/en/LC_MESSAGES/djangojs.po | 20 + .../views/locale/es/LC_MESSAGES/djangojs.mo | Bin 0 -> 445 bytes .../views/locale/es/LC_MESSAGES/djangojs.po | 21 + .../views/locale/fr/LC_MESSAGES/djangojs.mo | Bin 0 -> 436 bytes .../views/locale/fr/LC_MESSAGES/djangojs.po | 20 + tests/regressiontests/views/media/file.txt | 1 + tests/regressiontests/views/models.py | 26 + tests/regressiontests/views/tests/__init__.py | 4 + tests/regressiontests/views/tests/defaults.py | 39 + .../views/tests/generic/__init__.py | 0 .../views/tests/generic/date_based.py | 71 + tests/regressiontests/views/tests/i18n.py | 30 + tests/regressiontests/views/tests/static.py | 15 + tests/regressiontests/views/urls.py | 48 + tests/regressiontests/views/views.py | 7 + .../views/article_archive_month.html | 1 + tests/templates/views/article_detail.html | 1 + tests/urls.py | 3 + 96 files changed, 7303 insertions(+), 6228 deletions(-) create mode 100644 tests/regressiontests/auth_backends/__init__.py create mode 100644 tests/regressiontests/auth_backends/models.py create mode 100644 tests/regressiontests/auth_backends/tests.py create mode 100644 tests/regressiontests/forms/extra.py create mode 100644 tests/regressiontests/forms/fields.py create mode 100644 tests/regressiontests/forms/forms.py delete mode 100644 tests/regressiontests/forms/localflavor.py create mode 100644 tests/regressiontests/forms/localflavor/__init__.py create mode 100644 tests/regressiontests/forms/localflavor/ar.py create mode 100644 tests/regressiontests/forms/localflavor/au.py create mode 100644 tests/regressiontests/forms/localflavor/br.py create mode 100644 tests/regressiontests/forms/localflavor/ca.py create mode 100644 tests/regressiontests/forms/localflavor/ch.py create mode 100644 tests/regressiontests/forms/localflavor/cl.py create mode 100644 tests/regressiontests/forms/localflavor/de.py create mode 100644 tests/regressiontests/forms/localflavor/fi.py create mode 100644 tests/regressiontests/forms/localflavor/fr.py create mode 100644 tests/regressiontests/forms/localflavor/generic.py create mode 100644 tests/regressiontests/forms/localflavor/is_.py create mode 100644 tests/regressiontests/forms/localflavor/it.py create mode 100644 tests/regressiontests/forms/localflavor/jp.py create mode 100644 tests/regressiontests/forms/localflavor/nl.py create mode 100644 tests/regressiontests/forms/localflavor/pl.py create mode 100644 tests/regressiontests/forms/localflavor/sk.py create mode 100644 tests/regressiontests/forms/localflavor/uk.py create mode 100644 tests/regressiontests/forms/localflavor/us.py create mode 100644 tests/regressiontests/forms/widgets.py create mode 100644 tests/regressiontests/max_lengths/__init__.py create mode 100644 tests/regressiontests/max_lengths/models.py create mode 100644 tests/regressiontests/max_lengths/tests.py create mode 100644 tests/regressiontests/utils/timesince.py create mode 100644 tests/regressiontests/views/__init__.py create mode 100644 tests/regressiontests/views/fixtures/testdata.json create mode 100644 tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.mo create mode 100644 tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.po create mode 100644 tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.mo create mode 100644 tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.po create mode 100644 tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo create mode 100644 tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po create mode 100644 tests/regressiontests/views/media/file.txt create mode 100644 tests/regressiontests/views/models.py create mode 100644 tests/regressiontests/views/tests/__init__.py create mode 100644 tests/regressiontests/views/tests/defaults.py create mode 100644 tests/regressiontests/views/tests/generic/__init__.py create mode 100644 tests/regressiontests/views/tests/generic/date_based.py create mode 100644 tests/regressiontests/views/tests/i18n.py create mode 100644 tests/regressiontests/views/tests/static.py create mode 100644 tests/regressiontests/views/urls.py create mode 100644 tests/regressiontests/views/views.py create mode 100644 tests/templates/views/article_archive_month.html create mode 100644 tests/templates/views/article_detail.html diff --git a/AUTHORS b/AUTHORS index 9d47f2c876..b6c9141db9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -49,6 +49,7 @@ answer newbie questions, and generally made Django that much better: andy@jadedplanet.net Fabrice Aneche ant9000@netwise.it + Florian Apolloner David Ascher david@kazserve.org Arthur @@ -83,6 +84,7 @@ answer newbie questions, and generally made Django that much better: Russell Cloran colin@owlfish.com crankycoder@gmail.com + Paul Collier Pete Crosier Matt Croydon flavio.curella@gmail.com @@ -121,6 +123,7 @@ answer newbie questions, and generally made Django that much better: Afonso Fernández Nogueira Matthew Flanagan Eric Floehr + Vincent Foley Jorge Gajon gandalf@owca.info Marc Garcia @@ -194,6 +197,7 @@ answer newbie questions, and generally made Django that much better: Waylan Limberg limodou Philip Lindborg + msaelices Matt McClanahan Martin Maney masonsimon+django@gmail.com @@ -257,6 +261,7 @@ answer newbie questions, and generally made Django that much better: Brian Rosner Oliver Rutherfurd ryankanno + Manuel Saelices Ivan Sagalaev (Maniac) Vinay Sajip David Schein @@ -271,6 +276,7 @@ answer newbie questions, and generally made Django that much better: sopel Leo Soto Wiliam Alves de Souza + Don Spaulding Bjørn Stabell Georgi Stanojevski Vasiliy Stavenko diff --git a/django/bin/compile-messages.py b/django/bin/compile-messages.py index 2e1e908bbf..2838cb8aa4 100755 --- a/django/bin/compile-messages.py +++ b/django/bin/compile-messages.py @@ -4,20 +4,31 @@ import optparse import os import sys -def compile_messages(locale=None): - basedir = None +try: + set +except NameError: + from sets import Set as set # For Python 2.3 - if os.path.isdir(os.path.join('conf', 'locale')): - basedir = os.path.abspath(os.path.join('conf', 'locale')) - elif os.path.isdir('locale'): - basedir = os.path.abspath('locale') - else: - print "This script should be run from the Django SVN tree or your project or app tree." + +def compile_messages(locale=None): + basedirs = [os.path.join('conf', 'locale'), 'locale'] + if os.environ.get('DJANGO_SETTINGS_MODULE'): + from django.conf import settings + basedirs += settings.LOCALE_PATHS + + # Gather existing directories. + basedirs = set(map(os.path.abspath, filter(os.path.isdir, basedirs))) + + if not basedirs: + print "This script should be run from the Django SVN tree or your project or app tree, or with the settings module specified." sys.exit(1) - if locale is not None: - basedir = os.path.join(basedir, locale, 'LC_MESSAGES') + for basedir in basedirs: + if locale: + basedir = os.path.join(basedir, locale, 'LC_MESSAGES') + compile_messages_in_dir(basedir) +def compile_messages_in_dir(basedir): for dirpath, dirnames, filenames in os.walk(basedir): for f in filenames: if f.endswith('.po'): @@ -40,9 +51,13 @@ def main(): parser = optparse.OptionParser() parser.add_option('-l', '--locale', dest='locale', help="The locale to process. Default is to process all.") + parser.add_option('--settings', + help='Python path to settings module, e.g. "myproject.settings". If provided, all LOCALE_PATHS will be processed. If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be checked as well.') options, args = parser.parse_args() if len(args): parser.error("This program takes no arguments") + if options.settings: + os.environ['DJANGO_SETTINGS_MODULE'] = options.settings compile_messages(options.locale) if __name__ == "__main__": diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index cfbe9a1f95..7fa870e222 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -140,7 +140,7 @@ class AdminBoundField(object): def original_value(self): if self.original: - return self.original.__dict__[self.field.column] + return self.original.__dict__[self.field.attname] def existing_display(self): try: diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 4b8efcca46..be6cfede11 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -1,3 +1,4 @@ +from django.db import connection from django.contrib.auth.models import User class ModelBackend: @@ -14,6 +15,49 @@ class ModelBackend: except User.DoesNotExist: return None + def get_group_permissions(self, user_obj): + "Returns a list of permission strings that this user has through his/her groups." + if not hasattr(user_obj, '_group_perm_cache'): + cursor = connection.cursor() + # The SQL below works out to the following, after DB quoting: + # cursor.execute(""" + # SELECT ct."app_label", p."codename" + # FROM "auth_permission" p, "auth_group_permissions" gp, "auth_user_groups" ug, "django_content_type" ct + # WHERE p."id" = gp."permission_id" + # AND gp."group_id" = ug."group_id" + # AND ct."id" = p."content_type_id" + # AND ug."user_id" = %s, [self.id]) + qn = connection.ops.quote_name + sql = """ + SELECT ct.%s, p.%s + FROM %s p, %s gp, %s ug, %s ct + WHERE p.%s = gp.%s + AND gp.%s = ug.%s + AND ct.%s = p.%s + AND ug.%s = %%s""" % ( + qn('app_label'), qn('codename'), + qn('auth_permission'), qn('auth_group_permissions'), + qn('auth_user_groups'), qn('django_content_type'), + qn('id'), qn('permission_id'), + qn('group_id'), qn('group_id'), + qn('id'), qn('content_type_id'), + qn('user_id'),) + cursor.execute(sql, [user_obj.id]) + user_obj._group_perm_cache = set(["%s.%s" % (row[0], row[1]) for row in cursor.fetchall()]) + return user_obj._group_perm_cache + + def get_all_permissions(self, user_obj): + if not hasattr(user_obj, '_perm_cache'): + user_obj._perm_cache = set([u"%s.%s" % (p.content_type.app_label, p.codename) for p in user_obj.user_permissions.select_related()]) + user_obj._perm_cache.update(self.get_group_permissions(user_obj)) + return user_obj._perm_cache + + def has_perm(self, user_obj, perm): + return perm in self.get_all_permissions(user_obj) + + def has_module_perms(self, user_obj, app_label): + return bool(len([p for p in self.get_all_permissions(user_obj) if p[:p.index('.')] == app_label])) + def get_user(self, user_id): try: return User.objects.get(pk=user_id) diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 2f9954e742..33f92dc854 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -1,6 +1,7 @@ +from django.contrib import auth from django.core import validators from django.core.exceptions import ImproperlyConfigured -from django.db import connection, models +from django.db import models from django.db.models.manager import EmptyManager from django.contrib.contenttypes.models import ContentType from django.utils.encoding import smart_str @@ -210,64 +211,68 @@ class User(models.Model): return self.password != UNUSABLE_PASSWORD def get_group_permissions(self): - "Returns a list of permission strings that this user has through his/her groups." - if not hasattr(self, '_group_perm_cache'): - cursor = connection.cursor() - # The SQL below works out to the following, after DB quoting: - # cursor.execute(""" - # SELECT ct."app_label", p."codename" - # FROM "auth_permission" p, "auth_group_permissions" gp, "auth_user_groups" ug, "django_content_type" ct - # WHERE p."id" = gp."permission_id" - # AND gp."group_id" = ug."group_id" - # AND ct."id" = p."content_type_id" - # AND ug."user_id" = %s, [self.id]) - qn = connection.ops.quote_name - sql = """ - SELECT ct.%s, p.%s - FROM %s p, %s gp, %s ug, %s ct - WHERE p.%s = gp.%s - AND gp.%s = ug.%s - AND ct.%s = p.%s - AND ug.%s = %%s""" % ( - qn('app_label'), qn('codename'), - qn('auth_permission'), qn('auth_group_permissions'), - qn('auth_user_groups'), qn('django_content_type'), - qn('id'), qn('permission_id'), - qn('group_id'), qn('group_id'), - qn('id'), qn('content_type_id'), - qn('user_id'),) - cursor.execute(sql, [self.id]) - self._group_perm_cache = set(["%s.%s" % (row[0], row[1]) for row in cursor.fetchall()]) - return self._group_perm_cache + """ + Returns a list of permission strings that this user has through + his/her groups. This method queries all available auth backends. + """ + permissions = set() + for backend in auth.get_backends(): + if hasattr(backend, "get_group_permissions"): + permissions.update(backend.get_group_permissions(self)) + return permissions def get_all_permissions(self): - if not hasattr(self, '_perm_cache'): - self._perm_cache = set([u"%s.%s" % (p.content_type.app_label, p.codename) for p in self.user_permissions.select_related()]) - self._perm_cache.update(self.get_group_permissions()) - return self._perm_cache + permissions = set() + for backend in auth.get_backends(): + if hasattr(backend, "get_all_permissions"): + permissions.update(backend.get_all_permissions(self)) + return permissions def has_perm(self, perm): - "Returns True if the user has the specified permission." + """ + Returns True if the user has the specified permission. This method + queries all available auth backends, but returns immediately if any + backend returns True. Thus, a user who has permission from a single + auth backend is assumed to have permission in general. + """ + # Inactive users have no permissions. if not self.is_active: return False + + # Superusers have all permissions. if self.is_superuser: return True - return perm in self.get_all_permissions() + + # Otherwise we need to check the backends. + for backend in auth.get_backends(): + if hasattr(backend, "has_perm"): + if backend.has_perm(self, perm): + return True + return False def has_perms(self, perm_list): - "Returns True if the user has each of the specified permissions." + """Returns True if the user has each of the specified permissions.""" for perm in perm_list: if not self.has_perm(perm): return False return True def has_module_perms(self, app_label): - "Returns True if the user has any permissions in the given app label." + """ + Returns True if the user has any permissions in the given app + label. Uses pretty much the same logic as has_perm, above. + """ if not self.is_active: return False + if self.is_superuser: return True - return bool(len([p for p in self.get_all_permissions() if p[:p.index('.')] == app_label])) + + for backend in auth.get_backends(): + if hasattr(backend, "has_module_perms"): + if backend.has_module_perms(self, app_label): + return True + return False def get_and_delete_messages(self): messages = [] @@ -300,7 +305,12 @@ class User(models.Model): class Message(models.Model): """ - The message system is a lightweight way to queue messages for given users. A message is associated with a User instance (so it is only applicable for registered users). There's no concept of expiration or timestamps. Messages are created by the Django admin after successful actions. For example, "The poll Foo was created successfully." is a message. + The message system is a lightweight way to queue messages for given + users. A message is associated with a User instance (so it is only + applicable for registered users). There's no concept of expiration or + timestamps. Messages are created by the Django admin after successful + actions. For example, "The poll Foo was created successfully." is a + message. """ user = models.ForeignKey(User) message = models.TextField(_('message')) diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py index 9471706363..382212bb70 100644 --- a/django/contrib/sessions/backends/base.py +++ b/django/contrib/sessions/backends/base.py @@ -107,7 +107,7 @@ class SessionBase(object): try: return self._session_cache except AttributeError: - if self.session_key is None: + if self._session_key is None: self._session_cache = {} else: self._session_cache = self.load() diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py index 062acca323..221db5cc60 100644 --- a/django/contrib/sessions/backends/file.py +++ b/django/contrib/sessions/backends/file.py @@ -31,11 +31,12 @@ class SessionStore(SessionBase): try: session_file = open(self._key_to_file(), "rb") try: - session_data = self.decode(session_file.read()) - except(EOFError, SuspiciousOperation): - self._session_key = self._get_new_session_key() - self._session_cache = {} - self.save() + try: + session_data = self.decode(session_file.read()) + except(EOFError, SuspiciousOperation): + self._session_key = self._get_new_session_key() + self._session_cache = {} + self.save() finally: session_file.close() except(IOError): @@ -64,4 +65,4 @@ class SessionStore(SessionBase): pass def clean(self): - pass \ No newline at end of file + pass diff --git a/django/core/context_processors.py b/django/core/context_processors.py index 3c826b1a7d..55c19c17e8 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -13,11 +13,19 @@ def auth(request): """ Returns context variables required by apps that use Django's authentication system. + + If there is no 'user' attribute in the request, uses AnonymousUser (from + django.contrib.auth). """ + if hasattr(request, 'user'): + user = request.user + else: + from django.contrib.auth.models import AnonymousUser + user = AnonymousUser() return { - 'user': request.user, - 'messages': request.user.get_and_delete_messages(), - 'perms': PermWrapper(request.user), + 'user': user, + 'messages': user.get_and_delete_messages(), + 'perms': PermWrapper(user), } def debug(request): diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index f98566be96..d4f5e55011 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -42,8 +42,11 @@ class ModPythonRequest(http.HttpRequest): return '%s%s' % (self.path, self._req.args and ('?' + self._req.args) or '') def is_secure(self): - # Note: modpython 3.2.10+ has req.is_https(), but we need to support previous versions - return 'HTTPS' in self._req.subprocess_env and self._req.subprocess_env['HTTPS'] == 'on' + try: + return self._req.is_https() + except AttributeError: + # mod_python < 3.2.10 doesn't have req.is_https(). + return self._req.subprocess_env.get('HTTPS', '').lower() in ('on', '1') def _load_post_and_files(self): "Populates self._post and self._files" diff --git a/django/db/backends/ado_mssql/creation.py b/django/db/backends/ado_mssql/creation.py index 1411ca4d6a..d4ba8f2897 100644 --- a/django/db/backends/ado_mssql/creation.py +++ b/django/db/backends/ado_mssql/creation.py @@ -6,10 +6,10 @@ DATA_TYPES = { 'DateField': 'smalldatetime', 'DateTimeField': 'smalldatetime', 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', - 'ImageField': 'varchar(100)', + 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'int', 'IPAddressField': 'char(15)', 'NullBooleanField': 'bit', diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index b2b3992651..efb351c07e 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -10,10 +10,10 @@ DATA_TYPES = { 'DateField': 'date', 'DateTimeField': 'datetime', 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', - 'ImageField': 'varchar(100)', + 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', 'NullBooleanField': 'bool', diff --git a/django/db/backends/mysql_old/creation.py b/django/db/backends/mysql_old/creation.py index b2b3992651..efb351c07e 100644 --- a/django/db/backends/mysql_old/creation.py +++ b/django/db/backends/mysql_old/creation.py @@ -10,10 +10,10 @@ DATA_TYPES = { 'DateField': 'date', 'DateTimeField': 'datetime', 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', - 'ImageField': 'varchar(100)', + 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', 'NullBooleanField': 'bool', diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index d080b5d283..f4ada55ac6 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -13,10 +13,10 @@ DATA_TYPES = { 'DateField': 'DATE', 'DateTimeField': 'TIMESTAMP', 'DecimalField': 'NUMBER(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'NVARCHAR2(100)', - 'FilePathField': 'NVARCHAR2(100)', + 'FileField': 'NVARCHAR2(%(max_length)s)', + 'FilePathField': 'NVARCHAR2(%(max_length)s)', 'FloatField': 'DOUBLE PRECISION', - 'ImageField': 'NVARCHAR2(100)', + 'ImageField': 'NVARCHAR2(%(max_length)s)', 'IntegerField': 'NUMBER(11)', 'IPAddressField': 'VARCHAR2(15)', 'NullBooleanField': 'NUMBER(1) CHECK ((%(column)s IN (0,1)) OR (%(column)s IS NULL))', @@ -28,7 +28,7 @@ DATA_TYPES = { 'SmallIntegerField': 'NUMBER(11)', 'TextField': 'NCLOB', 'TimeField': 'TIMESTAMP', - 'URLField': 'VARCHAR2(200)', + 'URLField': 'VARCHAR2(%(max_length)s)', 'USStateField': 'CHAR(2)', } diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py index ceffea19e6..b3e374da27 100644 --- a/django/db/backends/postgresql/creation.py +++ b/django/db/backends/postgresql/creation.py @@ -10,10 +10,10 @@ DATA_TYPES = { 'DateField': 'date', 'DateTimeField': 'timestamp with time zone', 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', - 'ImageField': 'varchar(100)', + 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'inet', 'NullBooleanField': 'boolean', diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index eccb19a160..54b75f23be 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -9,10 +9,10 @@ DATA_TYPES = { 'DateField': 'date', 'DateTimeField': 'datetime', 'DecimalField': 'decimal', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'real', - 'ImageField': 'varchar(100)', + 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', 'NullBooleanField': 'bool', diff --git a/django/db/models/base.py b/django/db/models/base.py index 208dfadddd..e88eda6dd0 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -83,6 +83,11 @@ class Model(object): def _get_pk_val(self): return getattr(self, self._meta.pk.attname) + def _set_pk_val(self, value): + return setattr(self, self._meta.pk.attname, value) + + pk = property(_get_pk_val, _set_pk_val) + def __repr__(self): return smart_str(u'<%s: %s>' % (self.__class__.__name__, unicode(self))) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 5b7adb2fb9..b0b9ae2135 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -693,8 +693,7 @@ class DecimalField(Field): class EmailField(CharField): def __init__(self, *args, **kwargs): - if 'max_length' not in kwargs: - kwargs['max_length'] = 75 + kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) def get_internal_type(self): @@ -714,6 +713,7 @@ class EmailField(CharField): class FileField(Field): def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs): self.upload_to = upload_to + kwargs['max_length'] = kwargs.get('max_length', 100) Field.__init__(self, verbose_name, name, **kwargs) def get_db_prep_save(self, value): @@ -815,6 +815,7 @@ class FileField(Field): class FilePathField(Field): def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs): self.path, self.match, self.recursive = path, match, recursive + kwargs['max_length'] = kwargs.get('max_length', 100) Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): @@ -887,6 +888,11 @@ class IPAddressField(Field): def validate(self, field_data, all_data): validators.isValidIPAddress4(field_data, None) + def formfield(self, **kwargs): + defaults = {'form_class': forms.IPAddressField} + defaults.update(kwargs) + return super(IPAddressField, self).formfield(**defaults) + class NullBooleanField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): diff --git a/django/middleware/http.py b/django/middleware/http.py index 8db3e4a524..78e066c67b 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -55,6 +55,7 @@ class SetRemoteAddrFromForwardedFor(object): return None else: # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. - # Take just the first one. - real_ip = real_ip.split(",")[0] + # Take just the last one. + # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/ + real_ip = real_ip.split(",")[-1].strip() request.META['REMOTE_ADDR'] = real_ip diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 8fb1d4f392..a39987e1b5 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -26,7 +26,7 @@ __all__ = ( 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 'BooleanField', 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', - 'SplitDateTimeField', + 'SplitDateTimeField', 'IPAddressField', ) # These values, if given to to_python(), will trigger the self.required check. @@ -635,3 +635,11 @@ class SplitDateTimeField(MultiValueField): raise ValidationError(ugettext(u'Enter a valid 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): + def __init__(self, *args, **kwargs): + RegexField.__init__(self, ipv4_re, + error_message=ugettext(u'Enter a valid IPv4 address.'), + *args, **kwargs) diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 2b1caddeda..3196db339e 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -58,7 +58,7 @@ class BaseForm(StrAndUnicode): # information. Any improvements to the form API should be made to *this* # class, not to the Form class. def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - initial=None, error_class=ErrorList): + initial=None, error_class=ErrorList, label_suffix=':'): self.is_bound = data is not None or files is not None self.data = data or {} self.files = files or {} @@ -66,6 +66,7 @@ class BaseForm(StrAndUnicode): self.prefix = prefix self.initial = initial or {} self.error_class = error_class + self.label_suffix = label_suffix self._errors = None # Stores the errors after clean() has been called. # The base_fields class attribute is the *class-wide* definition of @@ -129,9 +130,10 @@ class BaseForm(StrAndUnicode): output.append(error_row % force_unicode(bf_errors)) if bf.label: label = escape(force_unicode(bf.label)) - # Only add a colon if the label does not end in punctuation. - if label[-1] not in ':?.!': - label += ':' + # Only add the suffix if the label does not end in punctuation. + if self.label_suffix: + if label[-1] not in ':?.!': + label += self.label_suffix label = bf.label_tag(label) or '' else: label = '' diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py index 93cfa1d8fa..9bb90416c4 100644 --- a/django/oldforms/__init__.py +++ b/django/oldforms/__init__.py @@ -447,7 +447,7 @@ class LargeTextField(TextField): self.field_name, self.rows, self.cols, escape(data)) class HiddenField(FormField): - def __init__(self, field_name, is_required=False, validator_list=None): + def __init__(self, field_name, is_required=False, validator_list=None, max_length=None): if validator_list is None: validator_list = [] self.field_name, self.is_required = field_name, is_required self.validator_list = validator_list[:] @@ -674,7 +674,7 @@ class CheckboxSelectMultipleField(SelectMultipleField): #################### class FileUploadField(FormField): - def __init__(self, field_name, is_required=False, validator_list=None): + def __init__(self, field_name, is_required=False, validator_list=None, max_length=None): if validator_list is None: validator_list = [] self.field_name, self.is_required = field_name, is_required self.validator_list = [self.isNonEmptyFile] + validator_list @@ -946,7 +946,7 @@ class IPAddressField(TextField): class FilePathField(SelectField): "A SelectField whose choices are the files in a given directory." - def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None): + def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None, max_length=None): import os from django.db.models import BLANK_CHOICE_DASH if match is not None: diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index ac890d5da6..40e99c3962 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -72,12 +72,23 @@ class SortedDict(dict): def items(self): return zip(self.keyOrder, self.values()) + def iteritems(self): + for key in self.keyOrder: + yield key, dict.__getitem__(self, key) + def keys(self): return self.keyOrder[:] + def iterkeys(self): + return iter(self.keyOrder) + def values(self): return [dict.__getitem__(self, k) for k in self.keyOrder] + def itervalues(self): + for key in self.keyOrder: + yield dict.__getitem__(self, key) + def update(self, dict): for k, v in dict.items(): self.__setitem__(k, v) @@ -91,6 +102,15 @@ class SortedDict(dict): "Returns the value of the item at the given zero-based index." return self[self.keyOrder[index]] + def insert(self, index, key, value): + "Inserts the key, value pair before the item with the given index." + if key in self.keyOrder: + n = self.keyOrder.index(key) + del self.keyOrder[n] + if n < index: index -= 1 + self.keyOrder.insert(index, key) + dict.__setitem__(self, key, value) + def copy(self): "Returns a copy of this object." # This way of initializing the copy means it works for subclasses, too. diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 0e6541c721..9a296c3f2d 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -166,8 +166,8 @@ class DateFormat(TimeFormat): def O(self): "Difference to Greenwich time in hours; e.g. '+0200'" - tz = self.timezone.utcoffset(self.data) - return u"%+03d%02d" % (tz.seconds // 3600, (tz.seconds // 60) % 60) + seconds = self.Z() + 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'" diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 455788e7d7..e1132c3ab3 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -1,11 +1,20 @@ -import datetime, math, time +import datetime +import time + from django.utils.tzinfo import LocalTimezone from django.utils.translation import ungettext, ugettext def timesince(d, now=None): """ - Takes two datetime objects and returns the time between then and now - as a nicely formatted string, e.g "10 minutes" + Takes two datetime objects and returns the time between d and now + as a nicely formatted string, e.g. "10 minutes". If d occurs after now, + then "0 minutes" is returned. + + Units used are years, months, weeks, days, hours, and minutes. + Seconds and microseconds are ignored. Up to two adjacent units will be + displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are + possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not. + Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since """ chunks = ( @@ -32,6 +41,9 @@ def timesince(d, now=None): # ignore microsecond part of 'd' since we removed it from 'now' delta = now - (d - datetime.timedelta(0, 0, d.microsecond)) since = delta.days * 24 * 60 * 60 + delta.seconds + if since <= 0: + # d is in the future compared to now, stop processing. + return u'0 ' + ugettext('minutes') for i, (seconds, name) in enumerate(chunks): count = since // seconds if count != 0: diff --git a/docs/apache_auth.txt b/docs/apache_auth.txt index 9beb1ba43a..cab57fe6d5 100644 --- a/docs/apache_auth.txt +++ b/docs/apache_auth.txt @@ -34,12 +34,12 @@ with the standard ``Auth*`` and ``Require`` directives:: If you're using Apache 2.2, you'll need to take a couple extra steps. You'll need to ensure that ``mod_auth_basic`` and ``mod_authz_user`` - are loaded. These might be compiled staticly into Apache, or you might + are loaded. These might be compiled statically into Apache, or you might need to use ``LoadModule`` to load them dynamically (as shown in the example at the bottom of this note). You'll also need to insert configuration directives that prevent Apache - from trying to use other authentication modules. Depnding on which other + from trying to use other authentication modules. Depending on which other authentication modules you have loaded, you might need one or more of the following directives:: diff --git a/docs/authentication.txt b/docs/authentication.txt index 713e86c140..aee9c5224a 100644 --- a/docs/authentication.txt +++ b/docs/authentication.txt @@ -1062,3 +1062,40 @@ object the first time a user authenticates:: return User.objects.get(pk=user_id) except User.DoesNotExist: return None + +Handling authorization in custom backends +----------------------------------------- + +Custom auth backends can provide their own permissions. + +The user model will delegate permission lookup functions +(``get_group_permissions()``, ``get_all_permissions()``, ``has_perm()``, and +``has_module_perms()``) to any authentication backend that implements these +functions. + +The permissions given to the user will be the superset of all permissions +returned by all backends. That is, Django grants a permission to a user that any +one backend grants. + +The simple backend above could implement permissions for the magic admin fairly +simply:: + + class SettingsBackend: + + # ... + + def has_perm(self, user_obj, perm): + if user_obj.username == settings.ADMIN_LOGIN: + return True + else: + return False + +This gives full permissions to the user granted access in the above example. Notice +that the backend auth functions all take the user object as an argument, and +they also accept the same arguments given to the associated ``User`` functions. + +A full authorization implementation can be found in +``django/contrib/auth/backends.py`` _, which is the default backend and queries +the ``auth_permission`` table most of the time. + +.. _django/contrib/auth/backends.py: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/backends.py diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 68fcad24fe..0f99987bad 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -645,11 +645,11 @@ Examples: To run the test server on port 7000 with ``fixture1`` and ``fixture2``:: django-admin.py testserver --addrport 7000 fixture1 fixture2 - django-admin.py testserver fixture1 fixture2 --addrport 8080 + django-admin.py testserver fixture1 fixture2 --addrport 7000 (The above statements are equivalent. We include both of them to demonstrate -that it doesn't matter whether the options come before or after the -``testserver`` command.) +that it doesn't matter whether the options come before or after the fixture +arguments.) To run on 1.2.3.4:7000 with a `test` fixture:: diff --git a/docs/email.txt b/docs/email.txt index 97bdec0037..17c2b2115a 100644 --- a/docs/email.txt +++ b/docs/email.txt @@ -328,7 +328,7 @@ attribute on the ``EmailMessage`` class to change the main content type. The major type will always be ``"text"``, but you can change it to the subtype. For example:: - msg = EmailMessage(subject, html_content, from_email, to) + msg = EmailMessage(subject, html_content, from_email, [to]) msg.content_subtype = "html" # Main content is now text/html msg.send() diff --git a/docs/i18n.txt b/docs/i18n.txt index 25191e9402..bf73c88008 100644 --- a/docs/i18n.txt +++ b/docs/i18n.txt @@ -669,8 +669,11 @@ To create message files, you use the same ``make-messages.py`` tool as with the Django message files. You only need to be in the right place -- in the directory where either the ``conf/locale`` (in case of the source tree) or the ``locale/`` (in case of app messages or project messages) directory are located. And you -use the same ``compile-messages.py`` to produce the binary ``django.mo`` files that -are used by ``gettext``. +use the same ``compile-messages.py`` to produce the binary ``django.mo`` files +that are used by ``gettext``. + +You can also run ``compile-message.py --settings=path.to.settings`` to make +the compiler process all the directories in your ``LOCALE_PATHS`` setting. Application message files are a bit complicated to discover -- they need the ``LocaleMiddleware``. If you don't use the middleware, only the Django message @@ -710,7 +713,7 @@ language choice in a ``django_language`` cookie. After setting the language choice, Django redirects the user, following this algorithm: - * Django looks for a ``next`` parameter in ``POST`` request. + * Django looks for a ``next`` parameter in the ``POST`` data. * If that doesn't exist, or is empty, Django tries the URL in the ``Referrer`` header. * If that's empty -- say, if a user's browser suppresses that header -- diff --git a/docs/install.txt b/docs/install.txt index 173f4941ee..2de8529d24 100644 --- a/docs/install.txt +++ b/docs/install.txt @@ -86,7 +86,7 @@ to create a temporary test database. .. _SQLite: http://www.sqlite.org/ .. _pysqlite: http://initd.org/tracker/pysqlite .. _MySQL backend: ../databases/ -.. _cx_Oracle: http://www.python.net/crew/atuining/cx_Oracle/ +.. _cx_Oracle: http://cx-oracle.sourceforge.net/ .. _Oracle: http://www.oracle.com/ .. _testing framework: ../testing/ diff --git a/docs/model-api.txt b/docs/model-api.txt index 1f0bb60285..a0844ea961 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -293,6 +293,10 @@ visiting its URL on your site. Don't allow that. .. _`strftime formatting`: http://docs.python.org/lib/module-time.html#l2h-1941 +**New in development version:** By default, ``FileField`` instances are +created as ``varchar(100)`` columns in your database. As with other fields, you +can change the maximum length using the ``max_length`` argument. + ``FilePathField`` ~~~~~~~~~~~~~~~~~ @@ -330,6 +334,10 @@ not the full path. So, this example:: because the ``match`` applies to the base filename (``foo.gif`` and ``bar.gif``). +**New in development version:** By default, ``FilePathField`` instances are +created as ``varchar(100)`` columns in your database. As with other fields, you +can change the maximum length using the ``max_length`` argument. + ``FloatField`` ~~~~~~~~~~~~~~ @@ -361,6 +369,11 @@ Requires the `Python Imaging Library`_. .. _Python Imaging Library: http://www.pythonware.com/products/pil/ .. _elsewhere: ../db-api/#get-foo-height-and-get-foo-width +**New in development version:** By default, ``ImageField`` instances are +created as ``varchar(100)`` columns in your database. As with other fields, you +can change the maximum length using the ``max_length`` argument. + + ``IntegerField`` ~~~~~~~~~~~~~~~~ @@ -1284,6 +1297,17 @@ won't add the automatic ``id`` column. Each model requires exactly one field to have ``primary_key=True``. +The ``pk`` property +------------------- +**New in Django development version** + +Regardless of whether you define a primary key field yourself, or let Django +supply one for you, each model will have a property called ``pk``. It behaves +like a normal attribute on the model, but is actually an alias for whichever +attribute is the primary key field for the model. You can read and set this +value, just as you would for any other attribute, and it will update the +correct field in the model. + Admin options ============= diff --git a/docs/newforms.txt b/docs/newforms.txt index 19c5fc247f..2c8f67ce32 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -513,6 +513,26 @@ include ``%s`` -- then the library will act as if ``auto_id`` is ``True``. By default, ``auto_id`` is set to the string ``'id_%s'``. +Normally, a colon (``:``) will be appended after any label name when a form is +rendered. It's possible to change the colon to another character, or omit it +entirely, using the ``label_suffix`` parameter:: + + >>> f = ContactForm(auto_id='id_for_%s', label_suffix='') + >>> print f.as_ul() +
  • +
  • +
  • +
  • + >>> f = ContactForm(auto_id='id_for_%s', label_suffix=' ->') + >>> print f.as_ul() +
  • +
  • +
  • +
  • + +Note that the label suffix is added only if the last character of the +label isn't a punctuation character (``.``, ``!``, ``?`` or ``:``) + Notes on field ordering ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1264,6 +1284,15 @@ When you use a ``FileField`` on a form, you must also remember to Takes two optional arguments for validation, ``max_value`` and ``min_value``. These control the range of values permitted in the field. +``IPAddressField`` +~~~~~~~~~~~~~~~~~~ + + * Default widget: ``TextInput`` + * Empty value: ``''`` (an empty string) + * Normalizes to: A Unicode object. + * Validates that the given value is a valid IPv4 address, using a regular + expression. + ``MultipleChoiceField`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1690,7 +1719,7 @@ the full list of conversions: ``ForeignKey`` ``ModelChoiceField`` (see below) ``ImageField`` ``ImageField`` ``IntegerField`` ``IntegerField`` - ``IPAddressField`` ``CharField`` + ``IPAddressField`` ``IPAddressField`` ``ManyToManyField`` ``ModelMultipleChoiceField`` (see below) ``NullBooleanField`` ``CharField`` diff --git a/docs/sessions.txt b/docs/sessions.txt index 023285f704..7fc607bb13 100644 --- a/docs/sessions.txt +++ b/docs/sessions.txt @@ -18,13 +18,13 @@ To enable session functionality, do the following: ``MIDDLEWARE_CLASSES`` contains ``'django.contrib.sessions.middleware.SessionMiddleware'``. The default ``settings.py`` created by ``django-admin.py startproject`` has ``SessionMiddleware`` activated. - + * Add ``'django.contrib.sessions'`` to your ``INSTALLED_APPS`` setting, and run ``manage.py syncdb`` to install the single database table that stores session data. - + **New in development version**: this step is optional if you're not using - the database session backend; see `configuring the session engine`_. + the database session backend; see `configuring the session engine`_. If you don't want to use sessions, you might as well remove the ``SessionMiddleware`` line from ``MIDDLEWARE_CLASSES`` and ``'django.contrib.sessions'`` @@ -50,7 +50,7 @@ To use file-based sessions, set the ``SESSION_ENGINE`` setting to You might also want to set the ``SESSION_FILE_PATH`` setting (which defaults to ``/tmp``) to control where Django stores session files. Be -sure to check that your web server has permissions to read and write to +sure to check that your Web server has permissions to read and write to this location. Using cache-based sessions @@ -64,8 +64,8 @@ you've configured your cache; see the `cache documentation`_ for details. .. note:: - You probably don't want to use cache-based sessions if you're not using - the memcached cache backend. The local memory and simple cache backends + You should probably only use cache-based sessions if you're using the + memcached cache backend. The local memory and simple cache backends don't retain data long enough to be good choices, and it'll be faster to use file or database sessions directly instead of sending everything through the file or database cache backends. @@ -194,25 +194,26 @@ Here's a typical usage example:: Using sessions out of views =========================== -The ``SessionStore`` which implements the session storage method can be imported -and a API is available to manipulate the session data outside of a view:: +**New in Django development version** - >>> from django.contrib.sessions.engines.db import SessionStore +An API is available to manipulate session data outside of a view:: + + >>> from django.contrib.sessions.backends.db import SessionStore >>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead') >>> s['last_login'] = datetime.datetime(2005, 8, 20, 13, 35, 10) >>> s['last_login'] datetime.datetime(2005, 8, 20, 13, 35, 0) >>> s.save() -Or if you are using the ``django.contrib.sessions.engine.db`` each -session is just a normal Django model. The ``Session`` model -is defined in ``django/contrib/sessions/models.py``. Because it's a normal -model, you can access sessions using the normal Django database API:: +If you're using the ``django.contrib.sessions.engine.db`` backend, each +session is just a normal Django model. The ``Session`` model is defined in +``django/contrib/sessions/models.py``. Because it's a normal model, you can +access sessions using the normal Django database API:: >>> from django.contrib.sessions.models import Session >>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead') >>> s.expire_date - datetime.datetime(2005, 8, 20, 13, 35, 12) + datetime.datetime(2005, 8, 20, 13, 35, 12) Note that you'll need to call ``get_decoded()`` to get the session dictionary. This is necessary because the dictionary is stored in an encoded format:: @@ -306,10 +307,10 @@ Default: ``django.contrib.sessions.backends.db`` Controls where Django stores session data. Valid values are: - * ``'django.contrib.sessions.backends.db'`` - * ``'django.contrib.sessions.backends.file'`` + * ``'django.contrib.sessions.backends.db'`` + * ``'django.contrib.sessions.backends.file'`` * ``'django.contrib.sessions.backends.cache'`` - + See `configuring the session engine`_ for more details. SESSION_FILE_PATH diff --git a/docs/settings.txt b/docs/settings.txt index 46fdd70258..e40374a822 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -257,10 +257,11 @@ The database backend to use. The build-in database backends are ``'postgresql_psycopg2'``, ``'postgresql'``, ``'mysql'``, ``'mysql_old'``, ``'sqlite3'``, ``'oracle'``, or ``'ado_mssql'``. -You can also use a database backend that doesn't ship with Django by -setting ``DATABASE_ENGINE`` to a fully-qualified path (i.e. +In the Django development version, you can use a database backend that doesn't +ship with Django by setting ``DATABASE_ENGINE`` to a fully-qualified path (i.e. ``mypackage.backends.whatever``). Writing a whole new database backend from -scratch is left as an exercise to the reader. +scratch is left as an exercise to the reader; see the other backends for +examples. DATABASE_HOST ------------- diff --git a/docs/templates.txt b/docs/templates.txt index 9adf15731b..cd436a987d 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -1275,17 +1275,23 @@ For example, if ``blog_date`` is a date instance representing midnight on 1 June 2006, and ``comment_date`` is a date instance for 08:00 on 1 June 2006, then ``{{ comment_date|timesince:blog_date }}`` would return "8 hours". +Minutes is the smallest unit used, and "0 minutes" will be returned for any +date that is in the future relative to the comparison point. + timeuntil ~~~~~~~~~ Similar to ``timesince``, except that it measures the time from now until the given date or datetime. For example, if today is 1 June 2006 and ``conference_date`` is a date instance holding 29 June 2006, then -``{{ conference_date|timeuntil }}`` will return "28 days". +``{{ conference_date|timeuntil }}`` will return "4 weeks". Takes an optional argument that is a variable containing the date to use as the comparison point (instead of *now*). If ``from_date`` contains 22 June -2006, then ``{{ conference_date|timeuntil:from_date }}`` will return "7 days". +2006, then ``{{ conference_date|timeuntil:from_date }}`` will return "1 week". + +Minutes is the smallest unit used, and "0 minutes" will be returned for any +date that is in the past relative to the comparison point. title ~~~~~ diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py index 0a09579761..58770ef2ce 100644 --- a/tests/modeltests/basic/models.py +++ b/tests/modeltests/basic/models.py @@ -33,6 +33,11 @@ __test__ = {'API_TESTS': """ >>> a.id 1L +# Models have a pk property that is an alias for the primary key attribute (by +# default, the 'id' attribute). +>>> a.pk +1L + # Access database columns via Python attributes. >>> a.headline 'Area man programs in Python' diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py index 375859c897..381d81b987 100644 --- a/tests/modeltests/custom_pk/models.py +++ b/tests/modeltests/custom_pk/models.py @@ -56,6 +56,15 @@ DoesNotExist: Employee matching query does not exist. >>> Employee.objects.filter(pk__in=['ABC123','XYZ456']) [, ] +# The primary key can be accessed via the pk property on the model. +>>> e = Employee.objects.get(pk='ABC123') +>>> e.pk +u'ABC123' + +# Or we can use the real attribute name for the primary key: +>>> e.employee_code +u'ABC123' + # Fran got married and changed her last name. >>> fran = Employee.objects.get(pk='XYZ456') >>> fran.last_name = 'Jones' diff --git a/tests/modeltests/fixtures/models.py b/tests/modeltests/fixtures/models.py index a1e2446e56..5b53115733 100644 --- a/tests/modeltests/fixtures/models.py +++ b/tests/modeltests/fixtures/models.py @@ -9,6 +9,7 @@ FIXTURE_DIRS setting. """ from django.db import models +from django.conf import settings class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') @@ -53,7 +54,13 @@ __test__ = {'API_TESTS': """ # object list is unaffected >>> Article.objects.all() [, , , , ] +"""} +# Database flushing does not work on MySQL with the default storage engine +# because it requires transaction support. +if settings.DATABASE_ENGINE not in ('mysql', 'mysql_old'): + __test__['API_TESTS'] += \ +""" # Reset the database representation of this app. This will delete all data. >>> management.call_command('flush', verbosity=0, interactive=False) >>> Article.objects.all() @@ -75,7 +82,7 @@ Multiple fixtures named 'fixture2' in '...fixtures'. Aborting. # Dump the current contents of the database as a JSON fixture >>> management.call_command('dumpdata', 'fixtures', format='json') [{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}] -"""} +""" from django.test import TestCase diff --git a/tests/regressiontests/auth_backends/__init__.py b/tests/regressiontests/auth_backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/auth_backends/models.py b/tests/regressiontests/auth_backends/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/auth_backends/tests.py b/tests/regressiontests/auth_backends/tests.py new file mode 100644 index 0000000000..71e58442e7 --- /dev/null +++ b/tests/regressiontests/auth_backends/tests.py @@ -0,0 +1,66 @@ +""" +>>> from django.contrib.auth.models import User, Group, Permission +>>> from django.contrib.contenttypes.models import ContentType + +# No Permissions assigned yet, should return False except for superuser + +>>> user = User.objects.create_user('test', 'test@example.com', 'test') +>>> user.has_perm("auth.test") +False +>>> user.is_staff=True +>>> user.save() +>>> user.has_perm("auth.test") +False +>>> user.is_superuser=True +>>> user.save() +>>> user.has_perm("auth.test") +True +>>> user.is_staff = False +>>> user.is_superuser = False +>>> user.save() +>>> user.has_perm("auth.test") +False +>>> content_type=ContentType.objects.get_for_model(Group) +>>> perm = Permission.objects.create(name="test", content_type=content_type, codename="test") +>>> user.user_permissions.add(perm) +>>> user.save() + +# reloading user to purge the _perm_cache + +>>> user = User.objects.get(username="test") +>>> user.get_all_permissions() +set([u'auth.test']) +>>> user.get_group_permissions() +set([]) +>>> user.has_module_perms("Group") +False +>>> user.has_module_perms("auth") +True +>>> perm = Permission.objects.create(name="test2", content_type=content_type, codename="test2") +>>> user.user_permissions.add(perm) +>>> user.save() +>>> perm = Permission.objects.create(name="test3", content_type=content_type, codename="test3") +>>> user.user_permissions.add(perm) +>>> user.save() +>>> user = User.objects.get(username="test") +>>> user.get_all_permissions() +set([u'auth.test2', u'auth.test', u'auth.test3']) +>>> user.has_perm('test') +False +>>> user.has_perm('auth.test') +True +>>> user.has_perms(['auth.test2', 'auth.test3']) +True +>>> perm = Permission.objects.create(name="test_group", content_type=content_type, codename="test_group") +>>> group = Group.objects.create(name='test_group') +>>> group.permissions.add(perm) +>>> group.save() +>>> user.groups.add(group) +>>> user = User.objects.get(username="test") +>>> user.get_all_permissions() +set([u'auth.test2', u'auth.test', u'auth.test3', u'auth.test_group']) +>>> user.get_group_permissions() +set([u'auth.test_group']) +>>> user.has_perms(['auth.test3', 'auth.test_group']) +True +""" \ No newline at end of file diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py index a455f21e67..db16d2c198 100644 --- a/tests/regressiontests/backends/models.py +++ b/tests/regressiontests/backends/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.db import connection class Square(models.Model): root = models.IntegerField() @@ -7,18 +8,27 @@ class Square(models.Model): def __unicode__(self): return "%s ** 2 == %s" % (self.root, self.square) +if connection.features.uses_case_insensitive_names: + t_convert = lambda x: x.upper() +else: + t_convert = lambda x: x +qn = connection.ops.quote_name + __test__ = {'API_TESTS': """ #4896: Test cursor.executemany >>> from django.db import connection >>> cursor = connection.cursor() ->>> cursor.executemany('INSERT INTO BACKENDS_SQUARE (ROOT, SQUARE) VALUES (%s, %s)', -... [(i, i**2) for i in range(-5, 6)]) and None or None +>>> opts = Square._meta +>>> f1, f2 = opts.get_field('root'), opts.get_field('square') +>>> query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' +... % (t_convert(opts.db_table), qn(f1.column), qn(f2.column))) +>>> cursor.executemany(query, [(i, i**2) for i in range(-5, 6)]) and None or None >>> Square.objects.order_by('root') [, , , , , , , , , , ] #4765: executemany with params=[] does nothing ->>> cursor.executemany('INSERT INTO BACKENDS_SQUARE (ROOT, SQUARE) VALUES (%s, %s)', []) and None or None +>>> cursor.executemany(query, []) and None or None >>> Square.objects.count() 11 diff --git a/tests/regressiontests/forms/extra.py b/tests/regressiontests/forms/extra.py new file mode 100644 index 0000000000..7f6175f649 --- /dev/null +++ b/tests/regressiontests/forms/extra.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +tests = r""" +>>> from django.newforms import * +>>> import datetime +>>> import time +>>> import re +>>> try: +... from decimal import Decimal +... except ImportError: +... from django.utils._decimal import Decimal + +############### +# Extra stuff # +############### + +The newforms library comes with some extra, higher-level Field and Widget +classes that demonstrate some of the library's abilities. + +# SelectDateWidget ############################################################ + +>>> from django.newforms.extras import SelectDateWidget +>>> w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016')) +>>> print w.render('mydate', '') + + + +>>> w.render('mydate', None) == w.render('mydate', '') +True +>>> print w.render('mydate', '2010-04-15') + + + + +Using a SelectDateWidget in a form: + +>>> class GetDate(Form): +... mydate = DateField(widget=SelectDateWidget) +>>> a = GetDate({'mydate_month':'4', 'mydate_day':'1', 'mydate_year':'2008'}) +>>> print a.is_valid() +True +>>> print a.cleaned_data['mydate'] +2008-04-01 + +As with any widget that implements get_value_from_datadict, +we must be prepared to accept the input from the "as_hidden" +rendering as well. + +>>> print a['mydate'].as_hidden() + +>>> b=GetDate({'mydate':'2008-4-1'}) +>>> print b.is_valid() +True +>>> print b.cleaned_data['mydate'] +2008-04-01 + + +# MultiWidget and MultiValueField ############################################# +# MultiWidgets are widgets composed of other widgets. They are usually +# combined with MultiValueFields - a field that is composed of other fields. +# MulitWidgets can themselved be composed of other MultiWidgets. +# SplitDateTimeWidget is one example of a MultiWidget. + +>>> class ComplexMultiWidget(MultiWidget): +... def __init__(self, attrs=None): +... widgets = ( +... TextInput(), +... SelectMultiple(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), +... SplitDateTimeWidget(), +... ) +... super(ComplexMultiWidget, self).__init__(widgets, attrs) +... +... def decompress(self, value): +... if value: +... data = value.split(',') +... return [data[0], data[1], datetime.datetime(*time.strptime(data[2], "%Y-%m-%d %H:%M:%S")[0:6])] +... return [None, None, None] +... def format_output(self, rendered_widgets): +... return u'\n'.join(rendered_widgets) +>>> w = ComplexMultiWidget() +>>> print w.render('name', 'some text,JP,2007-04-25 06:24:00') + + + + +>>> class ComplexField(MultiValueField): +... def __init__(self, required=True, widget=None, label=None, initial=None): +... fields = ( +... CharField(), +... MultipleChoiceField(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), +... SplitDateTimeField() +... ) +... super(ComplexField, self).__init__(fields, required, widget, label, initial) +... +... def compress(self, data_list): +... if data_list: +... return '%s,%s,%s' % (data_list[0],''.join(data_list[1]),data_list[2]) +... return None + +>>> f = ComplexField(widget=w) +>>> f.clean(['some text', ['J','P'], ['2007-04-25','6:24:00']]) +u'some text,JP,2007-04-25 06:24:00' +>>> f.clean(['some text',['X'], ['2007-04-25','6:24:00']]) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. X is not one of the available choices.'] + +# If insufficient data is provided, None is substituted +>>> f.clean(['some text',['JP']]) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> class ComplexFieldForm(Form): +... field1 = ComplexField(widget=w) +>>> f = ComplexFieldForm() +>>> print f + + + + +>>> f = ComplexFieldForm({'field1_0':'some text','field1_1':['J','P'], 'field1_2_0':'2007-04-25', 'field1_2_1':'06:24:00'}) +>>> print f + + + + +>>> f.cleaned_data +{'field1': u'some text,JP,2007-04-25 06:24:00'} + + +# IPAddressField ################################################################## + +>>> f = IPAddressField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('127.0.0.1') +u'127.0.0.1' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('127.0.0.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('1.2.3.4.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('256.125.1.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] + +>>> f = IPAddressField(required=False) +>>> f.clean('') +u'' +>>> f.clean(None) +u'' +>>> f.clean('127.0.0.1') +u'127.0.0.1' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('127.0.0.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('1.2.3.4.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('256.125.1.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] + +################################# +# Tests of underlying functions # +################################# + +# smart_unicode tests +>>> from django.utils.encoding import smart_unicode +>>> class Test: +... def __str__(self): +... return 'ŠĐĆŽćžšđ' +>>> class TestU: +... def __str__(self): +... return 'Foo' +... def __unicode__(self): +... return u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111' +>>> smart_unicode(Test()) +u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111' +>>> smart_unicode(TestU()) +u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111' +>>> smart_unicode(1) +u'1' +>>> smart_unicode('foo') +u'foo' + + +#################################### +# Test accessing errors in clean() # +#################################### + +>>> class UserForm(Form): +... username = CharField(max_length=10) +... password = CharField(widget=PasswordInput) +... def clean(self): +... data = self.cleaned_data +... if not self.errors: +... data['username'] = data['username'].lower() +... return data + +>>> f = UserForm({'username': 'SirRobin', 'password': 'blue'}) +>>> f.is_valid() +True +>>> f.cleaned_data['username'] +u'sirrobin' + +####################################### +# Test overriding ErrorList in a form # +####################################### + +>>> from django.newforms.util import ErrorList +>>> class DivErrorList(ErrorList): +... def __unicode__(self): +... return self.as_divs() +... def as_divs(self): +... if not self: return u'' +... return u'
    %s
    ' % ''.join([u'
    %s
    ' % e for e in self]) +>>> class CommentForm(Form): +... name = CharField(max_length=50, required=False) +... email = EmailField() +... comment = CharField() +>>> data = dict(email='invalid') +>>> f = CommentForm(data, auto_id=False, error_class=DivErrorList) +>>> print f.as_p() +

    Name:

    +
    Enter a valid e-mail address.
    +

    Email:

    +
    This field is required.
    +

    Comment:

    + +################################# +# Test multipart-encoded form # +################################# + +>>> class FormWithoutFile(Form): +... username = CharField() +>>> class FormWithFile(Form): +... username = CharField() +... file = FileField() +>>> class FormWithImage(Form): +... image = ImageField() + +>>> FormWithoutFile().is_multipart() +False +>>> FormWithFile().is_multipart() +True +>>> FormWithImage().is_multipart() +True + +""" diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py new file mode 100644 index 0000000000..3b93d70338 --- /dev/null +++ b/tests/regressiontests/forms/fields.py @@ -0,0 +1,1162 @@ +# -*- coding: utf-8 -*- +tests = r""" +>>> from django.newforms import * +>>> from django.newforms.widgets import RadioFieldRenderer +>>> import datetime +>>> import time +>>> import re +>>> try: +... from decimal import Decimal +... except ImportError: +... from django.utils._decimal import Decimal + + +########## +# Fields # +########## + +Each Field class does some sort of validation. Each Field has a clean() method, +which either raises django.newforms.ValidationError or returns the "clean" +data -- usually a Unicode object, but, in some rare cases, a list. + +Each Field's __init__() takes at least these parameters: + required -- Boolean that specifies whether the field is required. + True by default. + widget -- A Widget class, or instance of a Widget class, that should be + used for this Field when displaying it. Each Field has a default + Widget that it'll use if you don't specify this. In most cases, + the default widget is TextInput. + label -- A verbose name for this field, for use in displaying this field in + a form. By default, Django will use a "pretty" version of the form + field name, if the Field is part of a Form. + initial -- A value to use in this Field's initial display. This value is + *not* used as a fallback if data isn't given. + +Other than that, the Field subclasses have class-specific options for +__init__(). For example, CharField has a max_length option. + +# CharField ################################################################### + +>>> f = CharField() +>>> f.clean(1) +u'1' +>>> f.clean('hello') +u'hello' +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean([1, 2, 3]) +u'[1, 2, 3]' + +>>> f = CharField(required=False) +>>> f.clean(1) +u'1' +>>> f.clean('hello') +u'hello' +>>> f.clean(None) +u'' +>>> f.clean('') +u'' +>>> f.clean([1, 2, 3]) +u'[1, 2, 3]' + +CharField accepts an optional max_length parameter: +>>> f = CharField(max_length=10, required=False) +>>> f.clean('12345') +u'12345' +>>> f.clean('1234567890') +u'1234567890' +>>> f.clean('1234567890a') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 10 characters (it has 11).'] + +CharField accepts an optional min_length parameter: +>>> f = CharField(min_length=10, required=False) +>>> f.clean('') +u'' +>>> f.clean('12345') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 10 characters (it has 5).'] +>>> f.clean('1234567890') +u'1234567890' +>>> f.clean('1234567890a') +u'1234567890a' + +>>> f = CharField(min_length=10, required=True) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('12345') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 10 characters (it has 5).'] +>>> f.clean('1234567890') +u'1234567890' +>>> f.clean('1234567890a') +u'1234567890a' + +# IntegerField ################################################################ + +>>> f = IntegerField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('1') +1 +>>> isinstance(f.clean('1'), int) +True +>>> f.clean('23') +23 +>>> f.clean('a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a whole number.'] +>>> f.clean(42) +42 +>>> f.clean(3.14) +Traceback (most recent call last): +... +ValidationError: [u'Enter a whole number.'] +>>> f.clean('1 ') +1 +>>> f.clean(' 1') +1 +>>> f.clean(' 1 ') +1 +>>> f.clean('1a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a whole number.'] + +>>> f = IntegerField(required=False) +>>> f.clean('') +>>> repr(f.clean('')) +'None' +>>> f.clean(None) +>>> repr(f.clean(None)) +'None' +>>> f.clean('1') +1 +>>> isinstance(f.clean('1'), int) +True +>>> f.clean('23') +23 +>>> f.clean('a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a whole number.'] +>>> f.clean('1 ') +1 +>>> f.clean(' 1') +1 +>>> f.clean(' 1 ') +1 +>>> f.clean('1a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a whole number.'] + +IntegerField accepts an optional max_value parameter: +>>> f = IntegerField(max_value=10) +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(1) +1 +>>> f.clean(10) +10 +>>> f.clean(11) +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is less than or equal to 10.'] +>>> f.clean('10') +10 +>>> f.clean('11') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is less than or equal to 10.'] + +IntegerField accepts an optional min_value parameter: +>>> f = IntegerField(min_value=10) +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(1) +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is greater than or equal to 10.'] +>>> f.clean(10) +10 +>>> f.clean(11) +11 +>>> f.clean('10') +10 +>>> f.clean('11') +11 + +min_value and max_value can be used together: +>>> f = IntegerField(min_value=10, max_value=20) +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(1) +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is greater than or equal to 10.'] +>>> f.clean(10) +10 +>>> f.clean(11) +11 +>>> f.clean('10') +10 +>>> f.clean('11') +11 +>>> f.clean(20) +20 +>>> f.clean(21) +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is less than or equal to 20.'] + +# FloatField ################################################################## + +>>> f = FloatField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('1') +1.0 +>>> isinstance(f.clean('1'), float) +True +>>> f.clean('23') +23.0 +>>> f.clean('3.14') +3.1400000000000001 +>>> f.clean(3.14) +3.1400000000000001 +>>> f.clean(42) +42.0 +>>> f.clean('a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a number.'] +>>> f.clean('1.0 ') +1.0 +>>> f.clean(' 1.0') +1.0 +>>> f.clean(' 1.0 ') +1.0 +>>> f.clean('1.0a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a number.'] + +>>> f = FloatField(required=False) +>>> f.clean('') + +>>> f.clean(None) + +>>> f.clean('1') +1.0 + +FloatField accepts min_value and max_value just like IntegerField: +>>> f = FloatField(max_value=1.5, min_value=0.5) + +>>> f.clean('1.6') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is less than or equal to 1.5.'] +>>> f.clean('0.4') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is greater than or equal to 0.5.'] +>>> f.clean('1.5') +1.5 +>>> f.clean('0.5') +0.5 + +# DecimalField ################################################################ + +>>> f = DecimalField(max_digits=4, decimal_places=2) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('1') +Decimal("1") +>>> isinstance(f.clean('1'), Decimal) +True +>>> f.clean('23') +Decimal("23") +>>> f.clean('3.14') +Decimal("3.14") +>>> f.clean(3.14) +Decimal("3.14") +>>> f.clean(Decimal('3.14')) +Decimal("3.14") +>>> f.clean('a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a number.'] +>>> f.clean('1.0 ') +Decimal("1.0") +>>> f.clean(' 1.0') +Decimal("1.0") +>>> f.clean(' 1.0 ') +Decimal("1.0") +>>> f.clean('1.0a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a number.'] +>>> f.clean('123.45') +Traceback (most recent call last): +... +ValidationError: [u'Ensure that there are no more than 4 digits in total.'] +>>> f.clean('1.234') +Traceback (most recent call last): +... +ValidationError: [u'Ensure that there are no more than 2 decimal places.'] +>>> f.clean('123.4') +Traceback (most recent call last): +... +ValidationError: [u'Ensure that there are no more than 2 digits before the decimal point.'] +>>> f.clean('-12.34') +Decimal("-12.34") +>>> f.clean('-123.45') +Traceback (most recent call last): +... +ValidationError: [u'Ensure that there are no more than 4 digits in total.'] +>>> f.clean('-.12') +Decimal("-0.12") +>>> f.clean('-00.12') +Decimal("-0.12") +>>> f.clean('-000.12') +Decimal("-0.12") +>>> f.clean('-000.123') +Traceback (most recent call last): +... +ValidationError: [u'Ensure that there are no more than 2 decimal places.'] +>>> f.clean('-000.1234') +Traceback (most recent call last): +... +ValidationError: [u'Ensure that there are no more than 4 digits in total.'] +>>> f.clean('--0.12') +Traceback (most recent call last): +... +ValidationError: [u'Enter a number.'] + +>>> f = DecimalField(max_digits=4, decimal_places=2, required=False) +>>> f.clean('') + +>>> f.clean(None) + +>>> f.clean('1') +Decimal("1") + +DecimalField accepts min_value and max_value just like IntegerField: +>>> f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5')) + +>>> f.clean('1.6') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is less than or equal to 1.5.'] +>>> f.clean('0.4') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value is greater than or equal to 0.5.'] +>>> f.clean('1.5') +Decimal("1.5") +>>> f.clean('0.5') +Decimal("0.5") +>>> f.clean('.5') +Decimal("0.5") +>>> f.clean('00.50') +Decimal("0.50") + +# DateField ################################################################### + +>>> import datetime +>>> f = DateField() +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) +datetime.date(2006, 10, 25) +>>> f.clean('2006-10-25') +datetime.date(2006, 10, 25) +>>> f.clean('10/25/2006') +datetime.date(2006, 10, 25) +>>> f.clean('10/25/06') +datetime.date(2006, 10, 25) +>>> f.clean('Oct 25 2006') +datetime.date(2006, 10, 25) +>>> f.clean('October 25 2006') +datetime.date(2006, 10, 25) +>>> f.clean('October 25, 2006') +datetime.date(2006, 10, 25) +>>> f.clean('25 October 2006') +datetime.date(2006, 10, 25) +>>> f.clean('25 October, 2006') +datetime.date(2006, 10, 25) +>>> f.clean('2006-4-31') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('200a-10-25') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('25/10/06') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = DateField(required=False) +>>> f.clean(None) +>>> repr(f.clean(None)) +'None' +>>> f.clean('') +>>> repr(f.clean('')) +'None' + +DateField accepts an optional input_formats parameter: +>>> f = DateField(input_formats=['%Y %m %d']) +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.date(2006, 10, 25) +>>> f.clean('2006 10 25') +datetime.date(2006, 10, 25) + +The input_formats parameter overrides all default input formats, +so the default formats won't work unless you specify them: +>>> f.clean('2006-10-25') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('10/25/2006') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('10/25/06') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] + +# TimeField ################################################################### + +>>> import datetime +>>> f = TimeField() +>>> f.clean(datetime.time(14, 25)) +datetime.time(14, 25) +>>> f.clean(datetime.time(14, 25, 59)) +datetime.time(14, 25, 59) +>>> f.clean('14:25') +datetime.time(14, 25) +>>> f.clean('14:25:59') +datetime.time(14, 25, 59) +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid time.'] +>>> f.clean('1:24 p.m.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid time.'] + +TimeField accepts an optional input_formats parameter: +>>> f = TimeField(input_formats=['%I:%M %p']) +>>> f.clean(datetime.time(14, 25)) +datetime.time(14, 25) +>>> f.clean(datetime.time(14, 25, 59)) +datetime.time(14, 25, 59) +>>> f.clean('4:25 AM') +datetime.time(4, 25) +>>> f.clean('4:25 PM') +datetime.time(16, 25) + +The input_formats parameter overrides all default input formats, +so the default formats won't work unless you specify them: +>>> f.clean('14:30:45') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid time.'] + +# DateTimeField ############################################################### + +>>> import datetime +>>> f = DateTimeField() +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) +datetime.datetime(2006, 10, 25, 14, 30, 59) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) +datetime.datetime(2006, 10, 25, 14, 30, 59, 200) +>>> f.clean('2006-10-25 14:30:45') +datetime.datetime(2006, 10, 25, 14, 30, 45) +>>> f.clean('2006-10-25 14:30:00') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('2006-10-25 14:30') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('2006-10-25') +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean('10/25/2006 14:30:45') +datetime.datetime(2006, 10, 25, 14, 30, 45) +>>> f.clean('10/25/2006 14:30:00') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('10/25/2006 14:30') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('10/25/2006') +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean('10/25/06 14:30:45') +datetime.datetime(2006, 10, 25, 14, 30, 45) +>>> f.clean('10/25/06 14:30:00') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('10/25/06 14:30') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('10/25/06') +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date/time.'] +>>> f.clean('2006-10-25 4:30 p.m.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date/time.'] + +DateField accepts an optional input_formats parameter: +>>> f = DateTimeField(input_formats=['%Y %m %d %I:%M %p']) +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) +datetime.datetime(2006, 10, 25, 14, 30, 59) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) +datetime.datetime(2006, 10, 25, 14, 30, 59, 200) +>>> f.clean('2006 10 25 2:30 PM') +datetime.datetime(2006, 10, 25, 14, 30) + +The input_formats parameter overrides all default input formats, +so the default formats won't work unless you specify them: +>>> f.clean('2006-10-25 14:30:45') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date/time.'] + +>>> f = DateTimeField(required=False) +>>> f.clean(None) +>>> repr(f.clean(None)) +'None' +>>> f.clean('') +>>> repr(f.clean('')) +'None' + +# RegexField ################################################################## + +>>> f = RegexField('^\d[A-F]\d$') +>>> f.clean('2A2') +u'2A2' +>>> f.clean('3F3') +u'3F3' +>>> f.clean('3G3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] +>>> f.clean(' 2A2') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] +>>> f.clean('2A2 ') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = RegexField('^\d[A-F]\d$', required=False) +>>> f.clean('2A2') +u'2A2' +>>> f.clean('3F3') +u'3F3' +>>> f.clean('3G3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] +>>> f.clean('') +u'' + +Alternatively, RegexField can take a compiled regular expression: +>>> f = RegexField(re.compile('^\d[A-F]\d$')) +>>> f.clean('2A2') +u'2A2' +>>> f.clean('3F3') +u'3F3' +>>> f.clean('3G3') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] +>>> f.clean(' 2A2') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] +>>> f.clean('2A2 ') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] + +RegexField takes an optional error_message argument: +>>> f = RegexField('^\d\d\d\d$', error_message='Enter a four-digit number.') +>>> f.clean('1234') +u'1234' +>>> f.clean('123') +Traceback (most recent call last): +... +ValidationError: [u'Enter a four-digit number.'] +>>> f.clean('abcd') +Traceback (most recent call last): +... +ValidationError: [u'Enter a four-digit number.'] + +RegexField also access min_length and max_length parameters, for convenience. +>>> f = RegexField('^\d+$', min_length=5, max_length=10) +>>> f.clean('123') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 5 characters (it has 3).'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 5 characters (it has 3).'] +>>> f.clean('12345') +u'12345' +>>> f.clean('1234567890') +u'1234567890' +>>> f.clean('12345678901') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 10 characters (it has 11).'] +>>> f.clean('12345a') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid value.'] + +# EmailField ################################################################## + +>>> f = EmailField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('person@example.com') +u'person@example.com' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] +>>> f.clean('foo@') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] +>>> f.clean('foo@bar') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] + +>>> f = EmailField(required=False) +>>> f.clean('') +u'' +>>> f.clean(None) +u'' +>>> f.clean('person@example.com') +u'person@example.com' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] +>>> f.clean('foo@') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] +>>> f.clean('foo@bar') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] + +EmailField also access min_length and max_length parameters, for convenience. +>>> f = EmailField(min_length=10, max_length=15) +>>> f.clean('a@foo.com') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 10 characters (it has 9).'] +>>> f.clean('alf@foo.com') +u'alf@foo.com' +>>> f.clean('alf123456788@foo.com') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 15 characters (it has 20).'] + +# FileField ################################################################## + +>>> f = FileField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f.clean({}) +Traceback (most recent call last): +... +ValidationError: [u'No file was submitted.'] + +>>> f.clean('some content that is not a file') +Traceback (most recent call last): +... +ValidationError: [u'No file was submitted. Check the encoding type on the form.'] + +>>> f.clean({'filename': 'name', 'content':None}) +Traceback (most recent call last): +... +ValidationError: [u'The submitted file is empty.'] + +>>> f.clean({'filename': 'name', 'content':''}) +Traceback (most recent call last): +... +ValidationError: [u'The submitted file is empty.'] + +>>> type(f.clean({'filename': 'name', 'content':'Some File Content'})) + + +# URLField ################################################################## + +>>> f = URLField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('http://localhost') +u'http://localhost' +>>> f.clean('http://example.com') +u'http://example.com' +>>> f.clean('http://www.example.com') +u'http://www.example.com' +>>> f.clean('http://www.example.com:8000/test') +u'http://www.example.com:8000/test' +>>> f.clean('http://200.8.9.10') +u'http://200.8.9.10' +>>> f.clean('http://200.8.9.10:8000/test') +u'http://200.8.9.10:8000/test' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://example') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://example.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://.com') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] + +>>> f = URLField(required=False) +>>> f.clean('') +u'' +>>> f.clean(None) +u'' +>>> f.clean('http://example.com') +u'http://example.com' +>>> f.clean('http://www.example.com') +u'http://www.example.com' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://example') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://example.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://.com') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] + +URLField takes an optional verify_exists parameter, which is False by default. +This verifies that the URL is live on the Internet and doesn't return a 404 or 500: +>>> f = URLField(verify_exists=True) +>>> f.clean('http://www.google.com') # This will fail if there's no Internet connection +u'http://www.google.com' +>>> f.clean('http://example') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid URL.'] +>>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') # bad domain +Traceback (most recent call last): +... +ValidationError: [u'This URL appears to be a broken link.'] +>>> f.clean('http://google.com/we-love-microsoft.html') # good domain, bad page +Traceback (most recent call last): +... +ValidationError: [u'This URL appears to be a broken link.'] +>>> f = URLField(verify_exists=True, required=False) +>>> f.clean('') +u'' +>>> f.clean('http://www.google.com') # This will fail if there's no Internet connection +u'http://www.google.com' + +URLField also access min_length and max_length parameters, for convenience. +>>> f = URLField(min_length=15, max_length=20) +>>> f.clean('http://f.com') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 15 characters (it has 12).'] +>>> f.clean('http://example.com') +u'http://example.com' +>>> f.clean('http://abcdefghijklmnopqrstuvwxyz.com') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 20 characters (it has 37).'] + +URLField should prepend 'http://' if no scheme was given +>>> f = URLField(required=False) +>>> f.clean('example.com') +u'http://example.com' +>>> f.clean('') +u'' +>>> f.clean('https://example.com') +u'https://example.com' + +# BooleanField ################################################################ + +>>> f = BooleanField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(True) +True +>>> f.clean(False) +False +>>> f.clean(1) +True +>>> f.clean(0) +False +>>> f.clean('Django rocks') +True + +>>> f = BooleanField(required=False) +>>> f.clean('') +False +>>> f.clean(None) +False +>>> f.clean(True) +True +>>> f.clean(False) +False +>>> f.clean(1) +True +>>> f.clean(0) +False +>>> f.clean('Django rocks') +True + +# ChoiceField ################################################################# + +>>> f = ChoiceField(choices=[('1', '1'), ('2', '2')]) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(1) +u'1' +>>> f.clean('1') +u'1' +>>> f.clean('3') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] + +>>> f = ChoiceField(choices=[('1', '1'), ('2', '2')], required=False) +>>> f.clean('') +u'' +>>> f.clean(None) +u'' +>>> f.clean(1) +u'1' +>>> f.clean('1') +u'1' +>>> f.clean('3') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] + +>>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) +>>> f.clean('J') +u'J' +>>> f.clean('John') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] + +# NullBooleanField ############################################################ + +>>> f = NullBooleanField() +>>> f.clean('') +>>> f.clean(True) +True +>>> f.clean(False) +False +>>> f.clean(None) +>>> f.clean('1') +>>> f.clean('2') +>>> f.clean('3') +>>> f.clean('hello') + +# MultipleChoiceField ######################################################### + +>>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')]) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean([1]) +[u'1'] +>>> f.clean(['1']) +[u'1'] +>>> f.clean(['1', '2']) +[u'1', u'2'] +>>> f.clean([1, '2']) +[u'1', u'2'] +>>> f.clean((1, '2')) +[u'1', u'2'] +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a list of values.'] +>>> f.clean([]) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(()) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(['3']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] + +>>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')], required=False) +>>> f.clean('') +[] +>>> f.clean(None) +[] +>>> f.clean([1]) +[u'1'] +>>> f.clean(['1']) +[u'1'] +>>> f.clean(['1', '2']) +[u'1', u'2'] +>>> f.clean([1, '2']) +[u'1', u'2'] +>>> f.clean((1, '2')) +[u'1', u'2'] +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a list of values.'] +>>> f.clean([]) +[] +>>> f.clean(()) +[] +>>> f.clean(['3']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] + +# ComboField ################################################################## + +ComboField takes a list of fields that should be used to validate a value, +in that order. +>>> f = ComboField(fields=[CharField(max_length=20), EmailField()]) +>>> f.clean('test@example.com') +u'test@example.com' +>>> f.clean('longemailaddress@example.com') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 20 characters (it has 28).'] +>>> f.clean('not an e-mail') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = ComboField(fields=[CharField(max_length=20), EmailField()], required=False) +>>> f.clean('test@example.com') +u'test@example.com' +>>> f.clean('longemailaddress@example.com') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 20 characters (it has 28).'] +>>> f.clean('not an e-mail') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid e-mail address.'] +>>> f.clean('') +u'' +>>> f.clean(None) +u'' + +# SplitDateTimeField ########################################################## + +>>> f = SplitDateTimeField() +>>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)]) +datetime.datetime(2006, 1, 10, 7, 30) +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a list of values.'] +>>> f.clean(['hello', 'there']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.', u'Enter a valid time.'] +>>> f.clean(['2006-01-10', 'there']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid time.'] +>>> f.clean(['hello', '07:30']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] + +>>> f = SplitDateTimeField(required=False) +>>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)]) +datetime.datetime(2006, 1, 10, 7, 30) +>>> f.clean(['2006-01-10', '07:30']) +datetime.datetime(2006, 1, 10, 7, 30) +>>> f.clean(None) +>>> f.clean('') +>>> f.clean(['']) +>>> f.clean(['', '']) +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a list of values.'] +>>> f.clean(['hello', 'there']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.', u'Enter a valid time.'] +>>> f.clean(['2006-01-10', 'there']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid time.'] +>>> f.clean(['hello', '07:30']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean(['2006-01-10', '']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid time.'] +>>> f.clean(['2006-01-10']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid time.'] +>>> f.clean(['', '07:30']) +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +""" diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py new file mode 100644 index 0000000000..ed88e3a6bb --- /dev/null +++ b/tests/regressiontests/forms/forms.py @@ -0,0 +1,1606 @@ +# -*- coding: utf-8 -*- +tests = r""" +>>> from django.newforms import * +>>> import datetime +>>> import time +>>> import re +>>> try: +... from decimal import Decimal +... except ImportError: +... from django.utils._decimal import Decimal + +######### +# Forms # +######### + +A Form is a collection of Fields. It knows how to validate a set of data and it +knows how to render itself in a couple of default ways (e.g., an HTML table). +You can pass it data in __init__(), as a dictionary. + +# Form ######################################################################## + +>>> class Person(Form): +... first_name = CharField() +... last_name = CharField() +... birthday = DateField() + +Pass a dictionary to a Form's __init__(). +>>> p = Person({'first_name': u'John', 'last_name': u'Lennon', 'birthday': u'1940-10-9'}) +>>> p.is_bound +True +>>> p.errors +{} +>>> p.is_valid() +True +>>> p.errors.as_ul() +u'' +>>> p.errors.as_text() +u'' +>>> p.cleaned_data +{'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)} +>>> print p['first_name'] + +>>> print p['last_name'] + +>>> print p['birthday'] + +>>> print p['nonexistentfield'] +Traceback (most recent call last): +... +KeyError: "Key 'nonexistentfield' not found in Form" + +>>> for boundfield in p: +... print boundfield + + + +>>> for boundfield in p: +... print boundfield.label, boundfield.data +First name John +Last name Lennon +Birthday 1940-10-9 +>>> print p + + + + +Empty dictionaries are valid, too. +>>> p = Person({}) +>>> p.is_bound +True +>>> p.errors +{'first_name': [u'This field is required.'], 'last_name': [u'This field is required.'], 'birthday': [u'This field is required.']} +>>> p.is_valid() +False +>>> p.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'Person' object has no attribute 'cleaned_data' +>>> print p +
    • This field is required.
    +
    • This field is required.
    +
    • This field is required.
    +>>> print p.as_table() +
    • This field is required.
    +
    • This field is required.
    +
    • This field is required.
    +>>> print p.as_ul() +
    • This field is required.
  • +
    • This field is required.
  • +
    • This field is required.
  • +>>> print p.as_p() +
    • This field is required.
    +

    +
    • This field is required.
    +

    +
    • This field is required.
    +

    + +If you don't pass any values to the Form's __init__(), or if you pass None, +the Form will be considered unbound and won't do any validation. Form.errors +will be an empty dictionary *but* Form.is_valid() will return False. +>>> p = Person() +>>> p.is_bound +False +>>> p.errors +{} +>>> p.is_valid() +False +>>> p.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'Person' object has no attribute 'cleaned_data' +>>> print p + + + +>>> print p.as_table() + + + +>>> print p.as_ul() +
  • +
  • +
  • +>>> print p.as_p() +

    +

    +

    + +Unicode values are handled properly. +>>> p = Person({'first_name': u'John', 'last_name': u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111', 'birthday': '1940-10-9'}) +>>> p.as_table() +u'\n\n' +>>> p.as_ul() +u'
  • \n
  • \n
  • ' +>>> p.as_p() +u'

    \n

    \n

    ' + +>>> p = Person({'last_name': u'Lennon'}) +>>> p.errors +{'first_name': [u'This field is required.'], 'birthday': [u'This field is required.']} +>>> p.is_valid() +False +>>> p.errors.as_ul() +u'
    • first_name
      • This field is required.
    • birthday
      • This field is required.
    ' +>>> print p.errors.as_text() +* first_name + * This field is required. +* birthday + * This field is required. +>>> p.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'Person' object has no attribute 'cleaned_data' +>>> p['first_name'].errors +[u'This field is required.'] +>>> p['first_name'].errors.as_ul() +u'
    • This field is required.
    ' +>>> p['first_name'].errors.as_text() +u'* This field is required.' + +>>> p = Person() +>>> print p['first_name'] + +>>> print p['last_name'] + +>>> print p['birthday'] + + +cleaned_data will always *only* contain a key for fields defined in the +Form, even if you pass extra data when you define the Form. In this +example, we pass a bunch of extra fields to the form constructor, +but cleaned_data contains only the form's fields. +>>> data = {'first_name': u'John', 'last_name': u'Lennon', 'birthday': u'1940-10-9', 'extra1': 'hello', 'extra2': 'hello'} +>>> p = Person(data) +>>> p.is_valid() +True +>>> p.cleaned_data +{'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)} + +cleaned_data will include a key and value for *all* fields defined in the Form, +even if the Form's data didn't include a value for fields that are not +required. In this example, the data dictionary doesn't include a value for the +"nick_name" field, but cleaned_data includes it. For CharFields, it's set to the +empty string. +>>> class OptionalPersonForm(Form): +... first_name = CharField() +... last_name = CharField() +... nick_name = CharField(required=False) +>>> data = {'first_name': u'John', 'last_name': u'Lennon'} +>>> f = OptionalPersonForm(data) +>>> f.is_valid() +True +>>> f.cleaned_data +{'nick_name': u'', 'first_name': u'John', 'last_name': u'Lennon'} + +For DateFields, it's set to None. +>>> class OptionalPersonForm(Form): +... first_name = CharField() +... last_name = CharField() +... birth_date = DateField(required=False) +>>> data = {'first_name': u'John', 'last_name': u'Lennon'} +>>> f = OptionalPersonForm(data) +>>> f.is_valid() +True +>>> f.cleaned_data +{'birth_date': None, 'first_name': u'John', 'last_name': u'Lennon'} + +"auto_id" tells the Form to add an "id" attribute to each form element. +If it's a string that contains '%s', Django will use that as a format string +into which the field's name will be inserted. It will also put a