diff --git a/AUTHORS b/AUTHORS index 757ef481e9..f175a1034b 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 @@ -244,7 +245,6 @@ answer newbie questions, and generally made Django that much better: phil@produxion.net phil.h.smith@gmail.com Gustavo Picon - pigletto Luke Plant plisk Daniel Poelzleithner @@ -281,6 +281,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 @@ -325,6 +326,7 @@ answer newbie questions, and generally made Django that much better: Rachel Willmer Gary Wilson Jakub Wiśniowski + Maciej Wiśniowski wojtek ye7cakf02@sneakemail.com ymasuda@ethercube.com diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 44ee2e55dd..f9cd20966e 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -40,7 +40,7 @@ class FieldWidgetNode(template.Node): default = None def __init__(self, bound_field_var): - self.bound_field_var = bound_field_var + self.bound_field_var = template.Variable(bound_field_var) def get_nodelist(cls, klass): if klass not in cls.nodelists: @@ -64,7 +64,7 @@ class FieldWidgetNode(template.Node): get_nodelist = classmethod(get_nodelist) def render(self, context): - bound_field = template.resolve_variable(self.bound_field_var, context) + bound_field = self.bound_field_var.resolve(context) context.push() context['bound_field'] = bound_field 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 baf7e6e210..62e4a8bc59 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 @@ -195,64 +196,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 = [] @@ -285,7 +290,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/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index 1d4628978d..959cec4c7f 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -19,6 +19,8 @@ class CommentFormNode(template.Node): ratings_optional=False, ratings_required=False, rating_options='', is_public=True): self.content_type = content_type + if obj_id_lookup_var is not None: + obj_id_lookup_var = template.Variable(obj_id_lookup_var) self.obj_id_lookup_var, self.obj_id, self.free = obj_id_lookup_var, obj_id, free self.photos_optional, self.photos_required = photos_optional, photos_required self.ratings_optional, self.ratings_required = ratings_optional, ratings_required @@ -32,7 +34,7 @@ class CommentFormNode(template.Node): context.push() if self.obj_id_lookup_var is not None: try: - self.obj_id = template.resolve_variable(self.obj_id_lookup_var, context) + self.obj_id = self.obj_id_lookup_var.resolve(context) except template.VariableDoesNotExist: return '' # Validate that this object ID is valid for this content-type. @@ -75,6 +77,8 @@ class CommentFormNode(template.Node): class CommentCountNode(template.Node): def __init__(self, package, module, context_var_name, obj_id, var_name, free): self.package, self.module = package, module + if context_var_name is not None: + context_var_name = template.Variable(context_var_name) self.context_var_name, self.obj_id = context_var_name, obj_id self.var_name, self.free = var_name, free @@ -82,7 +86,7 @@ class CommentCountNode(template.Node): from django.conf import settings manager = self.free and FreeComment.objects or Comment.objects if self.context_var_name is not None: - self.obj_id = template.resolve_variable(self.context_var_name, context) + self.obj_id = self.context_var_name.resolve(context) comment_count = manager.filter(object_id__exact=self.obj_id, content_type__app_label__exact=self.package, content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count() @@ -92,6 +96,8 @@ class CommentCountNode(template.Node): class CommentListNode(template.Node): def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None): self.package, self.module = package, module + if context_var_name is not None: + context_var_name = template.Variable(context_var_name) self.context_var_name, self.obj_id = context_var_name, obj_id self.var_name, self.free = var_name, free self.ordering = ordering @@ -102,7 +108,7 @@ class CommentListNode(template.Node): get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma if self.context_var_name is not None: try: - self.obj_id = template.resolve_variable(self.context_var_name, context) + self.obj_id = self.context_var_name.resolve(context) except template.VariableDoesNotExist: return '' kwargs = { diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py index 382212bb70..e6be0482de 100644 --- a/django/contrib/sessions/backends/base.py +++ b/django/contrib/sessions/backends/base.py @@ -16,7 +16,6 @@ class SessionBase(object): """ Base class for all Session classes. """ - TEST_COOKIE_NAME = 'testcookie' TEST_COOKIE_VALUE = 'worked' @@ -59,7 +58,7 @@ class SessionBase(object): def delete_test_cookie(self): del self[self.TEST_COOKIE_NAME] - + def encode(self, session_dict): "Returns the given session dictionary pickled and encoded as a string." pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL) @@ -77,28 +76,33 @@ class SessionBase(object): # just return an empty dictionary (an empty session). except: return {} - + def _get_new_session_key(self): "Returns session key that isn't being used." # The random module is seeded when this Apache child is created. # Use settings.SECRET_KEY as added salt. + try: + pid = os.getpid() + except AttributeError: + # No getpid() in Jython, for example + pid = 1 while 1: - session_key = md5.new("%s%s%s%s" % (random.randint(0, sys.maxint - 1), - os.getpid(), time.time(), settings.SECRET_KEY)).hexdigest() + session_key = md5.new("%s%s%s%s" % (random.randint(0, sys.maxint - 1), + pid, time.time(), settings.SECRET_KEY)).hexdigest() if not self.exists(session_key): break return session_key - + def _get_session_key(self): if self._session_key: return self._session_key else: self._session_key = self._get_new_session_key() return self._session_key - + def _set_session_key(self, session_key): self._session_key = session_key - + session_key = property(_get_session_key, _set_session_key) def _get_session(self): @@ -114,9 +118,9 @@ class SessionBase(object): return self._session_cache _session = property(_get_session) - + # Methods that child classes must implement. - + def exists(self, session_key): """ Returns True if the given session_key already exists. @@ -140,4 +144,3 @@ class SessionBase(object): Loads the session data and returns a dictionary. """ raise NotImplementedError - diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index a4731652f5..e15dfccdf2 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -1,18 +1,104 @@ import django +from django.core.management.base import BaseCommand, CommandError, handle_default_options from optparse import OptionParser import os import sys +from imp import find_module # For backwards compatibility: get_version() used to be in this module. get_version = django.get_version -def load_command_class(name): +# A cache of loaded commands, so that call_command +# doesn't have to reload every time it is called +_commands = None + +def find_commands(management_dir): """ - Given a command name, returns the Command class instance. Raises - ImportError if it doesn't exist. + Given a path to a management directory, return a list of all the command names + that are available. Returns an empty list if no commands are defined. """ - # Let the ImportError propogate. - return getattr(__import__('django.core.management.commands.%s' % name, {}, {}, ['Command']), 'Command')() + command_dir = os.path.join(management_dir,'commands') + try: + return [f[:-3] for f in os.listdir(command_dir) if not f.startswith('_') and f.endswith('.py')] + except OSError: + return [] + +def find_management_module(app_name): + """ + Determine the path to the management module for the application named, + without acutally importing the application or the management module. + + Raises ImportError if the management module cannot be found for any reason. + """ + parts = app_name.split('.') + parts.append('management') + parts.reverse() + path = None + while parts: + part = parts.pop() + f,path,descr = find_module(part, path and [path] or None) + return path + +def load_command_class(app_name, name): + """ + Given a command name and an application name, returns the Command + class instance. All errors raised by the importation process + (ImportError, AttributeError) are allowed to propagate. + """ + return getattr(__import__('%s.management.commands.%s' % (app_name, name), + {}, {}, ['Command']), 'Command')() + +def get_commands(load_user_commands=True, project_directory=None): + """ + Returns a dictionary of commands against the application in which + those commands can be found. This works by looking for a + management.commands package in django.core, and in each installed + application -- if a commands package exists, all commands in that + package are registered. + + Core commands are always included; user-defined commands will also + be included if ``load_user_commands`` is True. If a project directory + is provided, the startproject command will be disabled, and the + startapp command will be modified to use that directory. + + The dictionary is in the format {command_name: app_name}. Key-value + pairs from this dictionary can then be used in calls to + load_command_class(app_name, command_name) + + If a specific version of a command must be loaded (e.g., with the + startapp command), the instantiated module can be placed in the + dictionary in place of the application name. + + The dictionary is cached on the first call, and reused on subsequent + calls. + """ + global _commands + if _commands is None: + _commands = dict([(name, 'django.core') + for name in find_commands(__path__[0])]) + if load_user_commands: + # Get commands from all installed apps + from django.conf import settings + for app_name in settings.INSTALLED_APPS: + try: + path = find_management_module(app_name) + _commands.update(dict([(name, app_name) + for name in find_commands(path)])) + except ImportError: + pass # No management module - ignore this app + + if project_directory: + # Remove the "startproject" command from self.commands, because + # that's a django-admin.py command, not a manage.py command. + del _commands['startproject'] + + # Override the startapp command so that it always uses the + # project_directory, not the current working directory + # (which is default). + from django.core.management.commands.startapp import ProjectCommand + _commands['startapp'] = ProjectCommand(project_directory) + + return _commands def call_command(name, *args, **options): """ @@ -25,8 +111,26 @@ def call_command(name, *args, **options): call_command('shell', plain=True) call_command('sqlall', 'myapp') """ - klass = load_command_class(name) + try: + app_name = get_commands()[name] + if isinstance(app_name, BaseCommand): + # If the command is already loaded, use it directly. + klass = app_name + else: + klass = load_command_class(app_name, name) + except KeyError: + raise CommandError, "Unknown command: %r" % name return klass.execute(*args, **options) + +class LaxOptionParser(OptionParser): + """ + An option parser that doesn't raise any errors on unknown options. + + This is needed because the --settings and --pythonpath options affect + the commands (and thus the options) that are available to the user. + """ + def error(self, msg): + pass class ManagementUtility(object): """ @@ -38,21 +142,9 @@ class ManagementUtility(object): def __init__(self, argv=None): self.argv = argv or sys.argv[:] self.prog_name = os.path.basename(self.argv[0]) - self.commands = self.default_commands() - - def default_commands(self): - """ - Returns a dictionary of instances of all available Command classes. - - This works by looking for and loading all Python modules in the - django.core.management.commands package. - - The dictionary is in the format {name: command_instance}. - """ - command_dir = os.path.join(__path__[0], 'commands') - names = [f[:-3] for f in os.listdir(command_dir) if not f.startswith('_') and f.endswith('.py')] - return dict([(name, load_command_class(name)) for name in names]) - + self.project_directory = None + self.user_commands = False + def main_help_text(self): """ Returns the script's main help text, as a string. @@ -61,7 +153,7 @@ class ManagementUtility(object): usage.append('Django command line tool, version %s' % django.get_version()) usage.append("Type '%s help ' for help on a specific subcommand." % self.prog_name) usage.append('Available subcommands:') - commands = self.commands.keys() + commands = get_commands(self.user_commands, self.project_directory).keys() commands.sort() for cmd in commands: usage.append(' %s' % cmd) @@ -74,16 +166,33 @@ class ManagementUtility(object): django-admin.py or manage.py) if it can't be found. """ try: - return self.commands[subcommand] + app_name = get_commands(self.user_commands, self.project_directory)[subcommand] + if isinstance(app_name, BaseCommand): + # If the command is already loaded, use it directly. + klass = app_name + else: + klass = load_command_class(app_name, subcommand) except KeyError: sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % (subcommand, self.prog_name)) sys.exit(1) - + return klass + def execute(self): """ Given the command-line arguments, this figures out which subcommand is being run, creates a parser appropriate to that command, and runs it. """ + # Preprocess options to extract --settings and --pythonpath. These options + # could affect the commands that are available, so they must be processed + # early + parser = LaxOptionParser(version=get_version(), + option_list=BaseCommand.option_list) + try: + options, args = parser.parse_args(self.argv) + handle_default_options(options) + except: + pass # Ignore any option errors at this point. + try: subcommand = self.argv[1] except IndexError: @@ -91,8 +200,8 @@ class ManagementUtility(object): sys.exit(1) if subcommand == 'help': - if len(self.argv) > 2: - self.fetch_command(self.argv[2]).print_help(self.prog_name, self.argv[2]) + if len(args) > 2: + self.fetch_command(args[2]).print_help(self.prog_name, args[2]) else: sys.stderr.write(self.main_help_text() + '\n') sys.exit(1) @@ -116,16 +225,9 @@ class ProjectManagementUtility(ManagementUtility): """ def __init__(self, argv, project_directory): super(ProjectManagementUtility, self).__init__(argv) - - # Remove the "startproject" command from self.commands, because - # that's a django-admin.py command, not a manage.py command. - del self.commands['startproject'] - - # Override the startapp command so that it always uses the - # project_directory, not the current working directory (which is default). - from django.core.management.commands.startapp import ProjectCommand - self.commands['startapp'] = ProjectCommand(project_directory) - + self.project_directory = project_directory + self.user_commands = True + def setup_environ(settings_mod): """ Configure the runtime environment. This can also be used by external diff --git a/django/core/management/base.py b/django/core/management/base.py index d883fe23dc..26ecef4ff5 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -9,6 +9,17 @@ import os class CommandError(Exception): pass +def handle_default_options(options): + """ + Include any default options that all commands should accept + here so that ManagementUtility can handle them before searching + for user commands. + """ + if options.settings: + os.environ['DJANGO_SETTINGS_MODULE'] = options.settings + if options.pythonpath: + sys.path.insert(0, options.pythonpath) + class BaseCommand(object): # Metadata about this command. option_list = ( @@ -55,10 +66,7 @@ class BaseCommand(object): def run_from_argv(self, argv): parser = self.create_parser(argv[0], argv[1]) options, args = parser.parse_args(argv[2:]) - if options.settings: - os.environ['DJANGO_SETTINGS_MODULE'] = options.settings - if options.pythonpath: - sys.path.insert(0, options.pythonpath) + handle_default_options(options) self.execute(*args, **options.__dict__) def execute(self, *args, **options): 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/fields/__init__.py b/django/db/models/fields/__init__.py index 597271c997..67f14f9baf 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -684,8 +684,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): @@ -705,6 +704,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): @@ -806,6 +806,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): diff --git a/django/db/models/query.py b/django/db/models/query.py index 23d0bac6c8..4d0d295e97 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1180,7 +1180,7 @@ def delete_objects(seen_objs): if field.rel and field.null and field.rel.to in seen_objs: setattr(instance, field.attname, None) - setattr(instance, cls._meta.pk.attname, None) dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) + setattr(instance, cls._meta.pk.attname, None) transaction.commit_unless_managed() diff --git a/django/middleware/http.py b/django/middleware/http.py index 78e066c67b..71cdf7aa5d 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -54,8 +54,7 @@ class SetRemoteAddrFromForwardedFor(object): except KeyError: return None else: - # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. - # 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() + # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. The + # client's IP will be the first one. + real_ip = real_ip.split(",")[0].strip() request.META['REMOTE_ADDR'] = real_ip 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/template/__init__.py b/django/template/__init__.py index 449e0d0c28..1cfd85be06 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -88,8 +88,6 @@ UNKNOWN_SOURCE="<unknown source>" tag_re = re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END), re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END))) -# matches if the string is valid number -number_re = re.compile(r'[-+]?(\d+|\d*\.\d+)$') # global dictionary of libraries that have been loaded using get_library libraries = {} @@ -564,18 +562,19 @@ class FilterExpression(object): elif constant_arg is not None: args.append((False, constant_arg.replace(r'\"', '"'))) elif var_arg: - args.append((True, var_arg)) + args.append((True, Variable(var_arg))) filter_func = parser.find_filter(filter_name) self.args_check(filter_name,filter_func, args) filters.append( (filter_func,args)) upto = match.end() if upto != len(token): raise TemplateSyntaxError, "Could not parse the remainder: '%s' from '%s'" % (token[upto:], token) - self.var, self.filters = var, filters + self.filters = filters + self.var = Variable(var) def resolve(self, context, ignore_failures=False): try: - obj = resolve_variable(self.var, context) + obj = self.var.resolve(context) except VariableDoesNotExist: if ignore_failures: obj = None @@ -595,7 +594,7 @@ class FilterExpression(object): if not lookup: arg_vals.append(arg) else: - arg_vals.append(resolve_variable(arg, context)) + arg_vals.append(arg.resolve(context)) obj = func(obj, *arg_vals) return obj @@ -637,37 +636,98 @@ class FilterExpression(object): def resolve_variable(path, context): """ Returns the resolved variable, which may contain attribute syntax, within - the given context. The variable may be a hard-coded string (if it begins - and ends with single or double quote marks). + the given context. + + Deprecated; use the Variable class instead. + """ + return Variable(path).resolve(context) - >>> c = {'article': {'section':'News'}} - >>> resolve_variable('article.section', c) - u'News' - >>> resolve_variable('article', c) - {'section': 'News'} - >>> class AClass: pass - >>> c = AClass() - >>> c.article = AClass() - >>> c.article.section = 'News' - >>> resolve_variable('article.section', c) - u'News' +class Variable(object): + """ + A template variable, resolvable against a given context. The variable may be + a hard-coded string (if it begins and ends with single or double quote + marks):: + + >>> c = {'article': {'section':'News'}} + >>> Variable('article.section').resolve(c) + u'News' + >>> Variable('article').resolve(c) + {'section': 'News'} + >>> class AClass: pass + >>> c = AClass() + >>> c.article = AClass() + >>> c.article.section = 'News' + >>> Variable('article.section').resolve(c) + u'News' (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') """ - if number_re.match(path): - number_type = '.' in path and float or int - current = number_type(path) - elif path[0] in ('"', "'") and path[0] == path[-1]: - current = path[1:-1] - else: + + def __init__(self, var): + self.var = var + self.literal = None + self.lookups = None + + try: + # First try to treat this variable as a number. + # + # Note that this could cause an OverflowError here that we're not + # catching. Since this should only happen at compile time, that's + # probably OK. + self.literal = float(var) + + # So it's a float... is it an int? If the original value contained a + # dot or an "e" then it was a float, not an int. + if '.' not in var and 'e' not in var.lower(): + self.literal = int(self.literal) + + # "2." is invalid + if var.endswith('.'): + raise ValueError + + except ValueError: + # A ValueError means that the variable isn't a number. + # If it's wrapped with quotes (single or double), then + # we're also dealing with a literal. + if var[0] in "\"'" and var[0] == var[-1]: + self.literal = var[1:-1] + + else: + # Otherwise we'll set self.lookups so that resolve() knows we're + # dealing with a bonafide variable + self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR)) + + def resolve(self, context): + """Resolve this variable against a given context.""" + if self.lookups is not None: + # We're dealing with a variable that needs to be resolved + return self._resolve_lookup(context) + else: + # We're dealing with a literal, so it's already been "resolved" + return self.literal + + def __repr__(self): + return "<%s: %r>" % (self.__class__.__name__, self.var) + + def __str__(self): + return self.var + + def _resolve_lookup(self, context): + """ + Performs resolution of a real variable (i.e. not a literal) against the + given context. + + As indicated by the method's name, this method is an implementation + detail and shouldn't be called by external code. Use Variable.resolve() + instead. + """ current = context - bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR) - while bits: + for bit in self.lookups: try: # dictionary lookup - current = current[bits[0]] + current = current[bit] except (TypeError, AttributeError, KeyError): try: # attribute lookup - current = getattr(current, bits[0]) + current = getattr(current, bit) if callable(current): if getattr(current, 'alters_data', False): current = settings.TEMPLATE_STRING_IF_INVALID @@ -685,27 +745,27 @@ def resolve_variable(path, context): raise except (TypeError, AttributeError): try: # list-index lookup - current = current[int(bits[0])] + current = current[int(bit)] except (IndexError, # list index out of range ValueError, # invalid literal for int() - KeyError, # current is a dict without `int(bits[0])` key + KeyError, # current is a dict without `int(bit)` key TypeError, # unsubscriptable object ): - raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bits[0], current)) # missing attribute + raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bit, current)) # missing attribute except Exception, e: if getattr(e, 'silent_variable_failure', False): current = settings.TEMPLATE_STRING_IF_INVALID else: raise - del bits[0] - if isinstance(current, (basestring, Promise)): - try: - current = force_unicode(current) - except UnicodeDecodeError: - # Failing to convert to unicode can happen sometimes (e.g. debug - # tracebacks). So we allow it in this particular instance. - pass - return current + + if isinstance(current, (basestring, Promise)): + try: + current = force_unicode(current) + except UnicodeDecodeError: + # Failing to convert to unicode can happen sometimes (e.g. debug + # tracebacks). So we allow it in this particular instance. + pass + return current class Node(object): def render(self, context): @@ -861,10 +921,10 @@ class Library(object): class SimpleNode(Node): def __init__(self, vars_to_resolve): - self.vars_to_resolve = vars_to_resolve + self.vars_to_resolve = map(Variable, vars_to_resolve) def render(self, context): - resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] + resolved_vars = [var.resolve(context) for var in self.vars_to_resolve] return func(*resolved_vars) compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, SimpleNode) @@ -883,10 +943,10 @@ class Library(object): class InclusionNode(Node): def __init__(self, vars_to_resolve): - self.vars_to_resolve = vars_to_resolve + self.vars_to_resolve = map(Variable, vars_to_resolve) def render(self, context): - resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] + resolved_vars = [var.resolve(context) for var in self.vars_to_resolve] if takes_context: args = [context] + resolved_vars else: diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 1fd6d02c70..f1fcf5fe90 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -1,6 +1,6 @@ "Default variable filters" -from django.template import resolve_variable, Library +from django.template import Variable, Library from django.conf import settings from django.utils.translation import ugettext, ungettext from django.utils.encoding import force_unicode, smart_str, iri_to_uri @@ -297,7 +297,8 @@ def dictsort(value, arg): Takes a list of dicts, returns that list sorted by the property given in the argument. """ - decorated = [(resolve_variable(u'var.' + arg, {u'var' : item}), item) for item in value] + var_resolve = Variable(arg).resolve + decorated = [(var_resolve(item), item) for item in value] decorated.sort() return [item[1] for item in decorated] @@ -306,7 +307,8 @@ def dictsortreversed(value, arg): Takes a list of dicts, returns that list sorted in reverse order by the property given in the argument. """ - decorated = [(resolve_variable(u'var.' + arg, {u'var' : item}), item) for item in value] + var_resolve = Variable(arg).resolve + decorated = [(var_resolve(item), item) for item in value] decorated.sort() decorated.reverse() return [item[1] for item in decorated] diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index e23295f732..151985bfdd 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1,6 +1,6 @@ "Default tags used by the template system, available to all templates." -from django.template import Node, NodeList, Template, Context, resolve_variable +from django.template import Node, NodeList, Template, Context, Variable from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END from django.template import get_library, Library, InvalidTemplateLibrary from django.conf import settings @@ -30,7 +30,7 @@ class CycleNode(Node): def render(self, context): self.counter += 1 value = self.cyclevars[self.counter % self.cyclevars_len] - value = resolve_variable(value, context) + value = Variable(value).resolve(context) if self.variable_name: context[self.variable_name] = value return value @@ -57,12 +57,12 @@ class FilterNode(Node): class FirstOfNode(Node): def __init__(self, vars): - self.vars = vars + self.vars = map(Variable, vars) def render(self, context): for var in self.vars: try: - value = resolve_variable(var, context) + value = var.resolve(context) except VariableDoesNotExist: continue if value: @@ -147,7 +147,7 @@ class IfChangedNode(Node): def __init__(self, nodelist, *varlist): self.nodelist = nodelist self._last_seen = None - self._varlist = varlist + self._varlist = map(Variable, varlist) def render(self, context): if 'forloop' in context and context['forloop']['first']: @@ -156,7 +156,7 @@ class IfChangedNode(Node): if self._varlist: # Consider multiple parameters. # This automatically behaves like a OR evaluation of the multiple variables. - compare_to = [resolve_variable(var, context) for var in self._varlist] + compare_to = [var.resolve(context) for var in self._varlist] else: compare_to = self.nodelist.render(context) except VariableDoesNotExist: @@ -175,7 +175,7 @@ class IfChangedNode(Node): class IfEqualNode(Node): def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): - self.var1, self.var2 = var1, var2 + self.var1, self.var2 = Variable(var1), Variable(var2) self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false self.negate = negate @@ -184,11 +184,11 @@ class IfEqualNode(Node): def render(self, context): try: - val1 = resolve_variable(self.var1, context) + val1 = self.var1.resolve(context) except VariableDoesNotExist: val1 = None try: - val2 = resolve_variable(self.var2, context) + val2 = self.var2.resolve(context) except VariableDoesNotExist: val2 = None if (self.negate and val1 != val2) or (not self.negate and val1 == val2): diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index 19f368711c..652fda11ce 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -1,4 +1,4 @@ -from django.template import TemplateSyntaxError, TemplateDoesNotExist, resolve_variable +from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable from django.template import Library, Node from django.template.loader import get_template, get_template_from_string, find_template_source from django.conf import settings @@ -99,11 +99,11 @@ class ConstantIncludeNode(Node): class IncludeNode(Node): def __init__(self, template_name): - self.template_name = template_name + self.template_name = Variable(template_name) def render(self, context): try: - template_name = resolve_variable(self.template_name, context) + template_name = self.template_name.resolve(context) t = get_template(template_name) return t.render(context) except TemplateSyntaxError, e: diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 1e85c6b5d1..d5b0741a61 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -1,4 +1,4 @@ -from django.template import Node, resolve_variable +from django.template import Node, Variable from django.template import TemplateSyntaxError, TokenParser, Library from django.template import TOKEN_TEXT, TOKEN_VAR from django.utils import translation @@ -32,11 +32,11 @@ class GetCurrentLanguageBidiNode(Node): class TranslateNode(Node): def __init__(self, value, noop): - self.value = value + self.value = Variable(value) self.noop = noop def render(self, context): - value = resolve_variable(self.value, context) + value = self.value.resolve(context) if self.noop: return value else: diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 40e99c3962..2f3c9bb568 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -149,7 +149,7 @@ class MultiValueDict(dict): dict.__init__(self, key_to_list_mapping) def __repr__(self): - return "" % dict.__repr__(self) + return "<%s: %s>" % (self.__class__.__name__, dict.__repr__(self)) def __getitem__(self, key): """ diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 69c3e9c28b..6daae4386d 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -1,5 +1,6 @@ import types import urllib +import datetime from django.utils.functional import Promise class StrAndUnicode(object): @@ -30,7 +31,7 @@ def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): If strings_only is True, don't convert (some) non-string-like objects. """ - if strings_only and isinstance(s, (types.NoneType, int, long)): + if strings_only and isinstance(s, (types.NoneType, int, long, datetime.datetime, datetime.time, float)): return s if not isinstance(s, basestring,): if hasattr(s, '__unicode__'): 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/db-api.txt b/docs/db-api.txt index 08b5391e3c..adca8b4d5c 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -952,7 +952,7 @@ Example:: If you pass ``in_bulk()`` an empty list, you'll get an empty dictionary. ``iterator()`` -~~~~~~~~~~~~ +~~~~~~~~~~~~~~ Evaluates the ``QuerySet`` (by performing the query) and returns an `iterator`_ over the results. A ``QuerySet`` typically reads all of diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 0f99987bad..f098dfa988 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -735,3 +735,32 @@ distribution. It enables tab-completion of ``django-admin.py`` and * Press [TAB] to see all available options. * Type ``sql``, then [TAB], to see all available options whose names start with ``sql``. + +Customized actions +================== + +**New in Django development version** + +If you want to add an action of your own to ``manage.py``, you can. +Simply add a ``management/commands`` directory to your application. +Each python module in that directory will be discovered and registered as +a command that can be executed as an action when you run ``manage.py``:: + + /fancy_blog + __init__.py + models.py + /management + __init__.py + /commands + __init__.py + explode.py + views.py + +In this example, ``explode`` command will be made available to any project +that includes the ``fancy_blog`` application in ``settings.INSTALLED_APPS``. + +The ``explode.py`` module has only one requirement -- it must define a class +called ``Command`` that extends ``django.core.management.base.BaseCommand``. + +For more details on how to define your own commands, look at the code for the +existing ``django-admin.py`` commands, in ``/django/core/management/commands``. diff --git a/docs/email.txt b/docs/email.txt index 17c2b2115a..2bad79ce33 100644 --- a/docs/email.txt +++ b/docs/email.txt @@ -100,31 +100,31 @@ mail_admins() ============= ``django.core.mail.mail_admins()`` is a shortcut for sending an e-mail to the -site admins, as defined in the `ADMINS setting`_. Here's the definition:: +site admins, as defined in the `ADMINS`_ setting. Here's the definition:: mail_admins(subject, message, fail_silently=False) ``mail_admins()`` prefixes the subject with the value of the -`EMAIL_SUBJECT_PREFIX setting`_, which is ``"[Django] "`` by default. +`EMAIL_SUBJECT_PREFIX`_ setting, which is ``"[Django] "`` by default. -The "From:" header of the e-mail will be the value of the `SERVER_EMAIL setting`_. +The "From:" header of the e-mail will be the value of the `SERVER_EMAIL`_ setting. This method exists for convenience and readability. -.. _ADMINS setting: ../settings/#admins -.. _EMAIL_SUBJECT_PREFIX setting: ../settings/#email-subject-prefix -.. _SERVER_EMAIL setting: ../settings/#server-email +.. _ADMINS: ../settings/#admins +.. _EMAIL_SUBJECT_PREFIX: ../settings/#email-subject-prefix +.. _SERVER_EMAIL: ../settings/#server-email mail_managers() function ======================== ``django.core.mail.mail_managers()`` is just like ``mail_admins()``, except it -sends an e-mail to the site managers, as defined in the `MANAGERS setting`_. +sends an e-mail to the site managers, as defined in the `MANAGERS`_ setting. Here's the definition:: mail_managers(subject, message, fail_silently=False) -.. _MANAGERS setting: ../settings/#managers +.. _MANAGERS: ../settings/#managers Examples ======== @@ -225,7 +225,7 @@ optional and can be set at any time prior to calling the ``send()`` method. * ``from_email``: The sender's address. Both ``fred@example.com`` and ``Fred `` forms are legal. If omitted, the - ``DEFAULT_FROM_EMAIL`` setting is used. + `DEFAULT_FROM_EMAIL`_ setting is used. * ``to``: A list or tuple of recipient addresses. @@ -297,6 +297,8 @@ The class has the following methods: message.attach_file('/images/weather_map.png') +.. _DEFAULT_FROM_EMAIL: ../settings/#default-from-email + Sending alternative content types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/generic_views.txt b/docs/generic_views.txt index 87b82f7adf..08ff01c372 100644 --- a/docs/generic_views.txt +++ b/docs/generic_views.txt @@ -800,9 +800,14 @@ specify the page number in the URL in one of two ways: variable. You can iterate over the list provided by ``page_range`` to create a link to every page of results. -These values and lists are is 1-based, not 0-based, so the first page would be +These values and lists are 1-based, not 0-based, so the first page would be represented as page ``1``. +An example of the use of pagination can be found in the `object pagination`_ +example model. + +.. _`object pagination`: ../models/pagination/ + **New in Django development version:** As a special case, you are also permitted to use diff --git a/docs/model-api.txt b/docs/model-api.txt index adb9cfceb1..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`` ~~~~~~~~~~~~~~~~ diff --git a/docs/newforms.txt b/docs/newforms.txt index 6ff96e3b98..54bd6306c7 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -1926,11 +1926,22 @@ of the model fields: .. note:: If you specify ``fields`` when creating a form with ``form_for_model()``, - make sure that the fields that are *not* specified can provide default - values, or are allowed to have a value of ``None``. If a field isn't - specified on a form, the object created from the form can't provide - a value for that attribute, which will prevent the new instance from - being saved. + then the fields that are *not* specified will not be set by the form's + ``save()`` method. Django will prevent any attempt to save an incomplete + model, so if the model does not allow the missing fields to be empty, and + does not provide a default value for the missing fields, any attempt to + ``save()`` a ``form_for_model`` with missing fields will fail. To avoid + this failure, you must use ``save(commit=False)`` and manually set any + extra required fields:: + + instance = form.save(commit=False) + instance.required_field = 'new value' + instance.save() + + See the `section on saving forms`_ for more details on using + ``save(commit=False)``. + +.. _section on saving forms: `The save() method`_ Overriding the default field types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/request_response.txt b/docs/request_response.txt index bf914fb5ff..7806886841 100644 --- a/docs/request_response.txt +++ b/docs/request_response.txt @@ -190,7 +190,7 @@ necessary because some HTML form elements, notably That means you can't change attributes of ``request.POST`` and ``request.GET`` directly. -``QueryDict`` implements the all standard dictionary methods, because it's a +``QueryDict`` implements all the standard dictionary methods, because it's a subclass of dictionary. Exceptions are outlined here: * ``__getitem__(key)`` -- Returns the value for the given key. If the key diff --git a/docs/sessions.txt b/docs/sessions.txt index ab7ea56aaa..96e8d36854 100644 --- a/docs/sessions.txt +++ b/docs/sessions.txt @@ -198,14 +198,14 @@ Using sessions out of views An API is available to manipulate session data outside of a view:: - >>> from django.contrib.sessions.engines.db import SessionStore + >>> 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() -If you're using the ``django.contrib.sessions.engine.db`` backend, each +If you're using the ``django.contrib.sessions.backends.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:: diff --git a/docs/settings.txt b/docs/settings.txt index e40374a822..7ad1f6441d 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -363,7 +363,7 @@ regular expression which will hide from the DEBUG view anything that contains be able to give backtraces without seeing sensitive (or offensive) settings. Still, note that there are always going to be sections of your debug output that -are inapporpriate for public consumption. File paths, configuration options, and +are inappropriate for public consumption. File paths, configuration options, and the like all give attackers extra information about your server. Never deploy a site with ``DEBUG`` turned on. diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 232f54061f..b6173ad39a 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -928,10 +928,36 @@ current context, available in the ``render`` method:: ``resolve_variable`` will try to resolve ``blog_entry.date_updated`` and then format it accordingly. -.. note:: - The ``resolve_variable()`` function will throw a ``VariableDoesNotExist`` - exception if it cannot resolve the string passed to it in the current - context of the page. +.. admonition:: New in development version: + + Variable resolution has changed in the development version of Django. + ``template.resolve_variable()`` is still available, but has been deprecated + in favor of a new ``template.Variable`` class. Using this class will usually + be more efficient than calling ``template.resolve_variable`` + + To use the ``Variable`` class, simply instantiate it with the name of the + variable to be resolved, and then call ``variable.resolve(context)``. So, + in the development version, the above example would be more correctly + written as: + + .. parsed-literal:: + + class FormatTimeNode(template.Node): + def __init__(self, date_to_be_formatted, format_string): + self.date_to_be_formatted = **Variable(date_to_be_formatted)** + self.format_string = format_string + + def render(self, context): + try: + actual_date = **self.date_to_be_formatted.resolve(context)** + return actual_date.strftime(self.format_string) + except template.VariableDoesNotExist: + return '' + + Changes are highlighted in bold. + +Variable resolution will throw a ``VariableDoesNotExist`` exception if it cannot +resolve the string passed to it in the current context of the page. Shortcut for simple tags ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py index a0a68d4bcc..a2388223f0 100644 --- a/tests/modeltests/serializers/models.py +++ b/tests/modeltests/serializers/models.py @@ -63,6 +63,9 @@ class Movie(models.Model): def __unicode__(self): return self.title + +class Score(models.Model): + score = models.FloatField() __test__ = {'API_TESTS':""" # Create some data: @@ -83,7 +86,7 @@ __test__ = {'API_TESTS':""" >>> a2 = Article( ... author = joe, ... headline = "Time to reform copyright", -... pub_date = datetime(2006, 6, 16, 13, 00)) +... pub_date = datetime(2006, 6, 16, 13, 00, 11, 345)) >>> a1.save(); a2.save() >>> a1.categories = [sports, op_ed] >>> a2.categories = [music, op_ed] @@ -181,7 +184,7 @@ __test__ = {'API_TESTS':""" # Serializer output can be restricted to a subset of fields >>> print serializers.serialize("json", Article.objects.all(), fields=('headline','pub_date')) -[{"pk": 1, "model": "serializers.article", "fields": {"headline": "Just kidding; I love TV poker", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 2, "model": "serializers.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 3, "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00"}}] +[{"pk": 1, "model": "serializers.article", "fields": {"headline": "Just kidding; I love TV poker", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 2, "model": "serializers.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:11"}}, {"pk": 3, "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00"}}] # Every string is serialized as a unicode object, also primary key # which is 'varchar' @@ -207,4 +210,11 @@ u'G\u0119\u015bl\u0105 ja\u017a\u0144' >>> print list(serializers.deserialize('json', serializers.serialize('json', [mv2])))[0].object.id None +# Serialization and deserialization of floats: +>>> sc = Score(score=3.4) +>>> print serializers.serialize("json", [sc]) +[{"pk": null, "model": "serializers.score", "fields": {"score": 3.4}}] +>>> print list(serializers.deserialize('json', serializers.serialize('json', [sc])))[0].object.score +3.4 + """} diff --git a/tests/modeltests/signals/models.py b/tests/modeltests/signals/models.py index d41142135e..1fb73828bb 100644 --- a/tests/modeltests/signals/models.py +++ b/tests/modeltests/signals/models.py @@ -54,7 +54,7 @@ Is updated pre_delete signal, Tom Smith instance.id is not None: True post_delete signal, Tom Smith -instance.id is None: True +instance.id is None: False >>> p2 = Person(first_name='James', last_name='Jones') >>> p2.id = 99999 @@ -73,7 +73,7 @@ Is created pre_delete signal, James Jones instance.id is not None: True post_delete signal, James Jones -instance.id is None: True +instance.id is None: False >>> Person.objects.all() [] diff --git a/tests/modeltests/user_commands/__init__.py b/tests/modeltests/user_commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/user_commands/management/__init__.py b/tests/modeltests/user_commands/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/user_commands/management/commands/__init__.py b/tests/modeltests/user_commands/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/user_commands/management/commands/dance.py b/tests/modeltests/user_commands/management/commands/dance.py new file mode 100644 index 0000000000..5886cd1d8f --- /dev/null +++ b/tests/modeltests/user_commands/management/commands/dance.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = "Dance around like a madman." + args = '' + requires_model_validation = True + + def handle(self, *args, **options): + print "I don't feel like dancing." \ No newline at end of file diff --git a/tests/modeltests/user_commands/models.py b/tests/modeltests/user_commands/models.py new file mode 100644 index 0000000000..5f96806dac --- /dev/null +++ b/tests/modeltests/user_commands/models.py @@ -0,0 +1,30 @@ +""" +37. User-registered management commands + +The manage.py utility provides a number of useful commands for managing a +Django project. If you want to add a utility command of your own, you can. + +The user-defined command 'dance' is defined in the management/commands +subdirectory of this test application. It is a simple command that responds +with a printed message when invoked. + +For more details on how to define your own manage.py commands, look at the +django.core.management.commands directory. This directory contains the +definitions for the base Django manage.py commands. +""" + +__test__ = {'API_TESTS': """ +>>> from django.core import management + +# Invoke a simple user-defined command +>>> management.call_command('dance') +I don't feel like dancing. + +# Invoke a command that doesn't exist +>>> management.call_command('explode') +Traceback (most recent call last): +... +CommandError: Unknown command: 'explode' + + +"""} \ No newline at end of file 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/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