summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Holovaty <adrian@holovaty.com>2007-02-25 05:50:24 +0000
committerAdrian Holovaty <adrian@holovaty.com>2007-02-25 05:50:24 +0000
commit8f1fb6da3a2eaae0e8c2351266900c1ae9d36403 (patch)
treef26d97b57f425ac38fb54c69f8bf6171514c2903
parent67e0248806e8adc03ddd81e9543f77b540ed1673 (diff)
newforms-admin: Merged to [4563]
git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@4565 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--AUTHORS5
-rw-r--r--django/contrib/localflavor/usa/forms.py15
-rw-r--r--django/db/models/fields/__init__.py6
-rw-r--r--django/db/models/fields/related.py12
-rw-r--r--django/db/models/query.py12
-rw-r--r--django/newforms/fields.py5
-rw-r--r--django/newforms/models.py100
-rw-r--r--django/template/__init__.py14
-rw-r--r--django/template/defaultfilters.py78
-rw-r--r--docs/templates_python.txt12
-rw-r--r--setup.py2
-rw-r--r--tests/modeltests/model_forms/models.py180
-rw-r--r--tests/regressiontests/defaultfilters/tests.py48
-rw-r--r--tests/regressiontests/forms/tests.py94
14 files changed, 548 insertions, 35 deletions
diff --git a/AUTHORS b/AUTHORS
index a930a0e9f2..2278fcca2e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -125,7 +125,7 @@ answer newbie questions, and generally made Django that much better:
Martin Maney <http://www.chipy.org/Martin_Maney>
masonsimon+django@gmail.com
Manuzhai
- Petar Marić
+ Petar Marić <http://www.petarmaric.com/>
mark@junklight.com
Yasushi Masuda <whosaysni@gmail.com>
mattycakes@gmail.com
@@ -166,7 +166,7 @@ answer newbie questions, and generally made Django that much better:
smurf@smurf.noris.de
sopel
Georgi Stanojevski <glisha@gmail.com>
- Thomas Steinacher <tom@eggdrop.ch>
+ Thomas Steinacher <http://www.eggdrop.ch/>
nowell strite
Radek Švarz <http://www.svarz.cz/translate/>
Swaroop C H <http://www.swaroopch.info>
@@ -183,6 +183,7 @@ answer newbie questions, and generally made Django that much better:
Milton Waddams
wam-djangobug@wamber.net
Dan Watson <http://theidioteque.net/>
+ Chris Wesseling <Chris.Wesseling@cwi.nl>
Rachel Willmer <http://www.willmer.com/kb/>
Gary Wilson <gary.wilson@gmail.com>
wojtek
diff --git a/django/contrib/localflavor/usa/forms.py b/django/contrib/localflavor/usa/forms.py
index 4d2cb5e90a..9461f4fe80 100644
--- a/django/contrib/localflavor/usa/forms.py
+++ b/django/contrib/localflavor/usa/forms.py
@@ -4,7 +4,11 @@ USA-specific Form helpers
from django.newforms import ValidationError
from django.newforms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.newforms.util import smart_unicode
from django.utils.translation import gettext
+import re
+
+phone_digits_re = re.compile(r'^(?:1-?)?(\d{3})[-\.]?(\d{3})[-\.]?(\d{4})$')
class USZipCodeField(RegexField):
def __init__(self, *args, **kwargs):
@@ -13,6 +17,17 @@ class USZipCodeField(RegexField):
error_message=gettext(u'Enter a zip code in the format XXXXX or XXXXX-XXXX.'),
*args, **kwargs)
+class USPhoneNumberField(Field):
+ def clean(self, value):
+ super(USPhoneNumberField, self).clean(value)
+ if value in EMPTY_VALUES:
+ return u''
+ value = re.sub('(\(|\)|\s+)', '', smart_unicode(value))
+ m = phone_digits_re.search(value)
+ if m:
+ return u'%s-%s-%s' % (m.group(1), m.group(2), m.group(3))
+ raise ValidationError(u'Phone numbers must be in XXX-XXX-XXXX format.')
+
class USStateField(Field):
"""
A form field that validates its input is a U.S. state name or abbreviation.
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 0f1390ed57..982297c8bf 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -750,6 +750,12 @@ class PhoneNumberField(IntegerField):
def validate(self, field_data, all_data):
validators.isValidPhone(field_data, all_data)
+ def formfield(self, **kwargs):
+ from django.contrib.localflavor.usa.forms import USPhoneNumberField
+ defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
+ defaults.update(kwargs)
+ return USPhoneNumberField(**defaults)
+
class PositiveIntegerField(IntegerField):
def get_manipulator_field_objs(self):
return [oldforms.PositiveIntegerField]
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index dcbaf358a8..30efb412a8 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -545,9 +545,9 @@ class ForeignKey(RelatedField, Field):
setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
def formfield(self, **kwargs):
- defaults = {'choices': self.get_choices_default(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
+ defaults = {'queryset': self.rel.to._default_manager.all(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
defaults.update(kwargs)
- return forms.ChoiceField(**defaults)
+ return forms.ModelChoiceField(**defaults)
class OneToOneField(RelatedField, IntegerField):
def __init__(self, to, to_field=None, **kwargs):
@@ -606,9 +606,9 @@ class OneToOneField(RelatedField, IntegerField):
cls._meta.one_to_one_field = self
def formfield(self, **kwargs):
- defaults = {'choices': self.get_choices_default(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
+ defaults = {'queryset': self.rel.to._default_manager.all(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
defaults.update(kwargs)
- return forms.ChoiceField(**kwargs)
+ return forms.ModelChoiceField(**kwargs)
class ManyToManyField(RelatedField, Field):
def __init__(self, to, **kwargs):
@@ -716,9 +716,9 @@ class ManyToManyField(RelatedField, Field):
# MultipleChoiceField takes a list of IDs.
if kwargs.get('initial') is not None:
kwargs['initial'] = [i._get_pk_val() for i in kwargs['initial']]
- defaults = {'choices': self.get_choices_default(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
+ defaults = {'queryset' : self.rel.to._default_manager.all(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
defaults.update(kwargs)
- return forms.MultipleChoiceField(**defaults)
+ return forms.ModelMultipleChoiceField(**defaults)
class ManyToOneRel(object):
def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None,
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 2209521d93..5b2c18b25e 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -194,7 +194,17 @@ class QuerySet(object):
yield obj
def count(self):
- "Performs a SELECT COUNT() and returns the number of records as an integer."
+ """
+ Performs a SELECT COUNT() and returns the number of records as an
+ integer.
+
+ If the queryset is already cached (i.e. self._result_cache is set) this
+ simply returns the length of the cached results set to avoid multiple
+ SELECT COUNT(*) calls.
+ """
+ if self._result_cache is not None:
+ return len(self._result_cache)
+
counter = self._clone()
counter._order_by = ()
counter._select_related = False
diff --git a/django/newforms/fields.py b/django/newforms/fields.py
index 0f082b9ee3..8e3da03470 100644
--- a/django/newforms/fields.py
+++ b/django/newforms/fields.py
@@ -339,8 +339,9 @@ class ChoiceField(Field):
def _set_choices(self, value):
# Setting choices also sets the choices on the widget.
- self._choices = value
- self.widget.choices = value
+ # choices can be any iterable, but we call list() on it because
+ # it will be consumed more than once.
+ self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
diff --git a/django/newforms/models.py b/django/newforms/models.py
index a938b6350e..616c7141e7 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -3,9 +3,14 @@ Helper functions for creating Form classes from Django models
and database field objects.
"""
+from django.utils.translation import gettext
+from util import ValidationError
from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList
+from fields import Field, ChoiceField
+from widgets import Select, SelectMultiple, MultipleHiddenInput
-__all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields')
+__all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
+ 'ModelChoiceField', 'ModelMultipleChoiceField')
def model_save(self, commit=True):
"""
@@ -31,9 +36,9 @@ def save_instance(form, instance, commit=True):
raise ValueError("The %s could not be changed because the data didn't validate." % opts.object_name)
clean_data = form.clean_data
for f in opts.fields:
- if isinstance(f, models.AutoField):
+ if not f.editable or isinstance(f, models.AutoField):
continue
- setattr(instance, f.attname, clean_data[f.name])
+ setattr(instance, f.name, clean_data[f.name])
if commit:
instance.save()
for f in opts.many_to_many:
@@ -63,6 +68,8 @@ def form_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfiel
opts = model._meta
field_list = []
for f in opts.fields + opts.many_to_many:
+ if not f.editable:
+ continue
formfield = formfield_callback(f)
if formfield:
field_list.append((f.name, formfield))
@@ -84,6 +91,8 @@ def form_for_instance(instance, form=BaseForm, formfield_callback=lambda f, **kw
opts = model._meta
field_list = []
for f in opts.fields + opts.many_to_many:
+ if not f.editable:
+ continue
current_value = f.value_from_object(instance)
formfield = formfield_callback(f, initial=current_value)
if formfield:
@@ -94,5 +103,88 @@ def form_for_instance(instance, form=BaseForm, formfield_callback=lambda f, **kw
def form_for_fields(field_list):
"Returns a Form class for the given list of Django database field instances."
- fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list])
+ fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list if f.editable])
return type('FormForFields', (BaseForm,), {'base_fields': fields})
+
+class QuerySetIterator(object):
+ def __init__(self, queryset, empty_label, cache_choices):
+ self.queryset, self.empty_label, self.cache_choices = queryset, empty_label, cache_choices
+
+ def __iter__(self):
+ if self.empty_label is not None:
+ yield (u"", self.empty_label)
+ for obj in self.queryset:
+ yield (obj._get_pk_val(), str(obj))
+ # Clear the QuerySet cache if required.
+ if not self.cache_choices:
+ self.queryset._result_cache = None
+
+class ModelChoiceField(ChoiceField):
+ "A ChoiceField whose choices are a model QuerySet."
+ # This class is a subclass of ChoiceField for purity, but it doesn't
+ # actually use any of ChoiceField's implementation.
+ def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
+ required=True, widget=Select, label=None, initial=None, help_text=None):
+ self.queryset = queryset
+ self.empty_label = empty_label
+ self.cache_choices = cache_choices
+ # Call Field instead of ChoiceField __init__() because we don't need
+ # ChoiceField.__init__().
+ Field.__init__(self, required, widget, label, initial, help_text)
+ self.widget.choices = self.choices
+
+ def _get_choices(self):
+ # If self._choices is set, then somebody must have manually set
+ # the property self.choices. In this case, just return self._choices.
+ if hasattr(self, '_choices'):
+ return self._choices
+ # Otherwise, execute the QuerySet in self.queryset to determine the
+ # choices dynamically. Return a fresh QuerySetIterator that has not
+ # been consumed. Note that we're instantiating a new QuerySetIterator
+ # *each* time _get_choices() is called (and, thus, each time
+ # self.choices is accessed) so that we can ensure the QuerySet has not
+ # been consumed.
+ return QuerySetIterator(self.queryset, self.empty_label, self.cache_choices)
+
+ def _set_choices(self, value):
+ # This method is copied from ChoiceField._set_choices(). It's necessary
+ # because property() doesn't allow a subclass to overwrite only
+ # _get_choices without implementing _set_choices.
+ self._choices = self.widget.choices = list(value)
+
+ choices = property(_get_choices, _set_choices)
+
+ def clean(self, value):
+ Field.clean(self, value)
+ if value in ('', None):
+ return None
+ try:
+ value = self.queryset.model._default_manager.get(pk=value)
+ except self.queryset.model.DoesNotExist:
+ raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.'))
+ return value
+
+class ModelMultipleChoiceField(ModelChoiceField):
+ "A MultipleChoiceField whose choices are a model QuerySet."
+ hidden_widget = MultipleHiddenInput
+ def __init__(self, queryset, cache_choices=False, required=True,
+ widget=SelectMultiple, label=None, initial=None, help_text=None):
+ super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices,
+ required, widget, label, initial, help_text)
+
+ def clean(self, value):
+ if self.required and not value:
+ raise ValidationError(gettext(u'This field is required.'))
+ elif not self.required and not value:
+ return []
+ if not isinstance(value, (list, tuple)):
+ raise ValidationError(gettext(u'Enter a list of values.'))
+ final_values = []
+ for val in value:
+ try:
+ obj = self.queryset.model._default_manager.get(pk=val)
+ except self.queryset.model.DoesNotExist:
+ raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val)
+ else:
+ final_values.append(obj)
+ return final_values
diff --git a/django/template/__init__.py b/django/template/__init__.py
index d01d73e15e..90fd13e1ce 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -580,6 +580,8 @@ class FilterExpression(object):
def args_check(name, func, provided):
provided = list(provided)
plen = len(provided)
+ # Check to see if a decorator is providing the real function.
+ func = getattr(func, '_decorated_function', func)
args, varargs, varkw, defaults = getargspec(func)
# First argument is filter input.
args.pop(0)
@@ -812,7 +814,7 @@ class Library(object):
raise InvalidTemplateLibrary, "Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function)
def tag_function(self,func):
- self.tags[func.__name__] = func
+ self.tags[getattr(func, "_decorated_function", func).__name__] = func
return func
def filter(self, name=None, filter_func=None):
@@ -836,7 +838,7 @@ class Library(object):
raise InvalidTemplateLibrary, "Unsupported arguments to Library.filter: (%r, %r)", (name, filter_func)
def filter_function(self, func):
- self.filters[func.__name__] = func
+ self.filters[getattr(func, "_decorated_function", func).__name__] = func
return func
def simple_tag(self,func):
@@ -850,9 +852,9 @@ class Library(object):
resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve]
return func(*resolved_vars)
- compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, SimpleNode)
+ compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, SimpleNode)
compile_func.__doc__ = func.__doc__
- self.tag(func.__name__, compile_func)
+ self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
return func
def inclusion_tag(self, file_name, context_class=Context, takes_context=False):
@@ -886,9 +888,9 @@ class Library(object):
self.nodelist = t.nodelist
return self.nodelist.render(context_class(dict))
- compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, InclusionNode)
+ compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode)
compile_func.__doc__ = func.__doc__
- self.tag(func.__name__, compile_func)
+ self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
return func
return dec
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index a95a5dff6f..b53375b42d 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -8,6 +8,38 @@ import random as random_module
register = Library()
+#######################
+# STRING DECORATOR #
+#######################
+
+def smart_string(obj):
+ # FUTURE: Unicode strings should probably be normalized to a specific
+ # encoding and non-unicode strings should be converted to unicode too.
+# if isinstance(obj, unicode):
+# obj = obj.encode(settings.DEFAULT_CHARSET)
+# else:
+# obj = unicode(obj, settings.DEFAULT_CHARSET)
+ # FUTURE: Replace dumb string logic below with cool unicode logic above.
+ if not isinstance(obj, basestring):
+ obj = str(obj)
+ return obj
+
+def stringfilter(func):
+ """
+ Decorator for filters which should only receive strings. The object passed
+ as the first positional argument will be converted to a string.
+ """
+ def _dec(*args, **kwargs):
+ if args:
+ args = list(args)
+ args[0] = smart_string(args[0])
+ return func(*args, **kwargs)
+
+ # Include a reference to the real function (used to check original
+ # arguments by the template parser).
+ _dec._decorated_function = getattr(func, '_decorated_function', func)
+ return _dec
+
###################
# STRINGS #
###################
@@ -16,16 +48,18 @@ register = Library()
def addslashes(value):
"Adds slashes - useful for passing strings to JavaScript, for example."
return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
+addslashes = stringfilter(addslashes)
def capfirst(value):
"Capitalizes the first character of the value"
- value = str(value)
return value and value[0].upper() + value[1:]
-
+capfirst = stringfilter(capfirst)
+
def fix_ampersands(value):
"Replaces ampersands with ``&amp;`` entities"
from django.utils.html import fix_ampersands
return fix_ampersands(value)
+fix_ampersands = stringfilter(fix_ampersands)
def floatformat(text, arg=-1):
"""
@@ -52,7 +86,7 @@ def floatformat(text, arg=-1):
try:
d = int(arg)
except ValueError:
- return str(f)
+ return smart_string(f)
m = f - int(f)
if not m and d < 0:
return '%d' % int(f)
@@ -69,22 +103,26 @@ def linenumbers(value):
for i, line in enumerate(lines):
lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line))
return '\n'.join(lines)
+linenumbers = stringfilter(linenumbers)
def lower(value):
"Converts a string into all lowercase"
return value.lower()
+lower = stringfilter(lower)
def make_list(value):
"""
Returns the value turned into a list. For an integer, it's a list of
digits. For a string, it's a list of characters.
"""
- return list(str(value))
+ return list(value)
+make_list = stringfilter(make_list)
def slugify(value):
"Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
value = re.sub('[^\w\s-]', '', value).strip().lower()
return re.sub('[-\s]+', '-', value)
+slugify = stringfilter(slugify)
def stringformat(value, arg):
"""
@@ -96,13 +134,14 @@ def stringformat(value, arg):
of Python string formatting
"""
try:
- return ("%" + arg) % value
+ return ("%" + str(arg)) % value
except (ValueError, TypeError):
return ""
def title(value):
"Converts a string into titlecase"
return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
+title = stringfilter(title)
def truncatewords(value, arg):
"""
@@ -118,6 +157,7 @@ def truncatewords(value, arg):
if not isinstance(value, basestring):
value = str(value)
return truncate_words(value, length)
+truncatewords = stringfilter(truncatewords)
def truncatewords_html(value, arg):
"""
@@ -133,10 +173,12 @@ def truncatewords_html(value, arg):
if not isinstance(value, basestring):
value = str(value)
return truncate_html_words(value, length)
+truncatewords_html = stringfilter(truncatewords_html)
def upper(value):
"Converts a string into all uppercase"
return value.upper()
+upper = stringfilter(upper)
def urlencode(value):
"Escapes a value for use in a URL"
@@ -144,11 +186,13 @@ def urlencode(value):
if not isinstance(value, basestring):
value = str(value)
return urllib.quote(value)
+urlencode = stringfilter(urlencode)
def urlize(value):
"Converts URLs in plain text into clickable links"
from django.utils.html import urlize
return urlize(value, nofollow=True)
+urlize = stringfilter(urlize)
def urlizetrunc(value, limit):
"""
@@ -159,10 +203,12 @@ def urlizetrunc(value, limit):
"""
from django.utils.html import urlize
return urlize(value, trim_url_limit=int(limit), nofollow=True)
+urlizetrunc = stringfilter(urlizetrunc)
def wordcount(value):
"Returns the number of words"
return len(value.split())
+wordcount = stringfilter(wordcount)
def wordwrap(value, arg):
"""
@@ -171,7 +217,8 @@ def wordwrap(value, arg):
Argument: number of characters to wrap the text at.
"""
from django.utils.text import wrap
- return wrap(str(value), int(arg))
+ return wrap(value, int(arg))
+wordwrap = stringfilter(wordwrap)
def ljust(value, arg):
"""
@@ -179,7 +226,8 @@ def ljust(value, arg):
Argument: field size
"""
- return str(value).ljust(int(arg))
+ return value.ljust(int(arg))
+ljust = stringfilter(ljust)
def rjust(value, arg):
"""
@@ -187,15 +235,18 @@ def rjust(value, arg):
Argument: field size
"""
- return str(value).rjust(int(arg))
+ return value.rjust(int(arg))
+rjust = stringfilter(rjust)
def center(value, arg):
"Centers the value in a field of a given width"
- return str(value).center(int(arg))
+ return value.center(int(arg))
+center = stringfilter(center)
def cut(value, arg):
"Removes all values of arg from the given string"
return value.replace(arg, '')
+cut = stringfilter(cut)
###################
# HTML STRINGS #
@@ -205,15 +256,18 @@ def escape(value):
"Escapes a string's HTML"
from django.utils.html import escape
return escape(value)
+escape = stringfilter(escape)
def linebreaks(value):
"Converts newlines into <p> and <br />s"
from django.utils.html import linebreaks
return linebreaks(value)
+linebreaks = stringfilter(linebreaks)
def linebreaksbr(value):
"Converts newlines into <br />s"
return value.replace('\n', '<br />')
+linebreaksbr = stringfilter(linebreaksbr)
def removetags(value, tags):
"Removes a space separated list of [X]HTML tags from the output"
@@ -224,13 +278,13 @@ def removetags(value, tags):
value = starttag_re.sub('', value)
value = endtag_re.sub('', value)
return value
+removetags = stringfilter(removetags)
def striptags(value):
"Strips all [X]HTML tags"
from django.utils.html import strip_tags
- if not isinstance(value, basestring):
- value = str(value)
return strip_tags(value)
+striptags = stringfilter(striptags)
###################
# LISTS #
@@ -265,7 +319,7 @@ def first(value):
def join(value, arg):
"Joins a list with a string, like Python's ``str.join(list)``"
try:
- return arg.join(map(str, value))
+ return arg.join(map(smart_string, value))
except AttributeError: # fail silently but nicely
return value
diff --git a/docs/templates_python.txt b/docs/templates_python.txt
index a6b565ed5c..5dd8e4fde0 100644
--- a/docs/templates_python.txt
+++ b/docs/templates_python.txt
@@ -654,6 +654,18 @@ decorator instead::
If you leave off the ``name`` argument, as in the second example above, Django
will use the function's name as the filter name.
+Template filters which expect strings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+If you are writing a template filter which only expects a string as the first
+argument, you should use the included decorator ``stringfilter`` which will convert
+an object to it's string value before being passed to your function::
+
+ from django import template
+
+ @template.stringfilter
+ def lower(value):
+ return value.lower()
+
Writing custom template tags
----------------------------
diff --git a/setup.py b/setup.py
index 13ad065681..6fd6fab816 100644
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ for dirpath, dirnames, filenames in os.walk(django_dir):
# Small hack for working with bdist_wininst.
# See http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html
-if sys.argv[1] == 'bdist_wininst':
+if len(sys.argv) > 1 and sys.argv[1] == 'bdist_wininst':
for file_info in data_files:
file_info[0] = '/PURELIB/%s' % file_info[0]
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
index 657d506b33..e64174a23f 100644
--- a/tests/modeltests/model_forms/models.py
+++ b/tests/modeltests/model_forms/models.py
@@ -40,13 +40,27 @@ class Writer(models.Model):
class Article(models.Model):
headline = models.CharField(maxlength=50)
pub_date = models.DateField()
+ created = models.DateField(editable=False)
writer = models.ForeignKey(Writer)
article = models.TextField()
categories = models.ManyToManyField(Category, blank=True)
+ def save(self):
+ import datetime
+ if not self.id:
+ self.created = datetime.date.today()
+ return super(Article, self).save()
+
def __str__(self):
return self.headline
+class PhoneNumber(models.Model):
+ phone = models.PhoneNumberField()
+ description = models.CharField(maxlength=20)
+
+ def __str__(self):
+ return self.phone
+
__test__ = {'API_TESTS': """
>>> from django.newforms import form_for_model, form_for_instance, save_instance, BaseForm, Form, CharField
>>> import datetime
@@ -281,4 +295,170 @@ existing Category instance.
<Category: Third>
>>> Category.objects.get(id=3)
<Category: Third>
+
+Here, we demonstrate that choices for a ForeignKey ChoiceField are determined
+at runtime, based on the data in the database when the form is displayed, not
+the data in the database when the form is instantiated.
+>>> ArticleForm = form_for_model(Article)
+>>> f = ArticleForm(auto_id=False)
+>>> print f.as_ul()
+<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
+<li>Pub date: <input type="text" name="pub_date" /></li>
+<li>Writer: <select name="writer">
+<option value="" selected="selected">---------</option>
+<option value="1">Mike Royko</option>
+<option value="2">Bob Woodward</option>
+</select></li>
+<li>Article: <textarea name="article"></textarea></li>
+<li>Categories: <select multiple="multiple" name="categories">
+<option value="1">Entertainment</option>
+<option value="2">It&#39;s a test</option>
+<option value="3">Third</option>
+</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
+>>> Category.objects.create(name='Fourth', url='4th')
+<Category: Fourth>
+>>> Writer.objects.create(name='Carl Bernstein')
+<Writer: Carl Bernstein>
+>>> print f.as_ul()
+<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
+<li>Pub date: <input type="text" name="pub_date" /></li>
+<li>Writer: <select name="writer">
+<option value="" selected="selected">---------</option>
+<option value="1">Mike Royko</option>
+<option value="2">Bob Woodward</option>
+<option value="3">Carl Bernstein</option>
+</select></li>
+<li>Article: <textarea name="article"></textarea></li>
+<li>Categories: <select multiple="multiple" name="categories">
+<option value="1">Entertainment</option>
+<option value="2">It&#39;s a test</option>
+<option value="3">Third</option>
+<option value="4">Fourth</option>
+</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
+
+# ModelChoiceField ############################################################
+
+>>> from django.newforms import ModelChoiceField, ModelMultipleChoiceField
+
+>>> f = ModelChoiceField(Category.objects.all())
+>>> 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(0)
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
+>>> f.clean(3)
+<Category: Third>
+>>> f.clean(2)
+<Category: It's a test>
+
+# Add a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.create(name='Fifth', url='5th')
+<Category: Fifth>
+>>> f.clean(5)
+<Category: Fifth>
+
+# Delete a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.get(url='5th').delete()
+>>> f.clean(5)
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
+
+>>> f = ModelChoiceField(Category.objects.filter(pk=1), required=False)
+>>> print f.clean('')
+None
+>>> f.clean('')
+>>> f.clean('1')
+<Category: Entertainment>
+>>> f.clean('100')
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
+
+# ModelMultipleChoiceField ####################################################
+
+>>> f = ModelMultipleChoiceField(Category.objects.all())
+>>> 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])
+[<Category: Entertainment>]
+>>> f.clean([2])
+[<Category: It's a test>]
+>>> f.clean(['1'])
+[<Category: Entertainment>]
+>>> f.clean(['1', '2'])
+[<Category: Entertainment>, <Category: It's a test>]
+>>> f.clean([1, '2'])
+[<Category: Entertainment>, <Category: It's a test>]
+>>> f.clean((1, '2'))
+[<Category: Entertainment>, <Category: It's a test>]
+>>> f.clean(['100'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 100 is not one of the available choices.']
+>>> f.clean('hello')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a list of values.']
+
+# Add a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.create(id=6, name='Sixth', url='6th')
+<Category: Sixth>
+>>> f.clean([6])
+[<Category: Sixth>]
+
+# Delete a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.get(url='6th').delete()
+>>> f.clean([6])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 6 is not one of the available choices.']
+
+>>> f = ModelMultipleChoiceField(Category.objects.all(), required=False)
+>>> f.clean([])
+[]
+>>> f.clean(())
+[]
+>>> f.clean(['10'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
+>>> f.clean(['3', '10'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
+>>> f.clean(['1', '10'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
+
+# PhoneNumberField ############################################################
+
+>>> PhoneNumberForm = form_for_model(PhoneNumber)
+>>> f = PhoneNumberForm({'phone': '(312) 555-1212', 'description': 'Assistance'})
+>>> f.is_valid()
+True
+>>> f.clean_data
+{'phone': u'312-555-1212', 'description': u'Assistance'}
"""}
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index b4ec9a0b03..c850806052 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -388,7 +388,53 @@ False
>>> phone2numeric('0800 flowers')
'0800 3569377'
-
+# Filters shouldn't break if passed non-strings
+>>> addslashes(123)
+'123'
+>>> linenumbers(123)
+'1. 123'
+>>> lower(123)
+'123'
+>>> make_list(123)
+['1', '2', '3']
+>>> slugify(123)
+'123'
+>>> title(123)
+'123'
+>>> truncatewords(123, 2)
+'123'
+>>> upper(123)
+'123'
+>>> urlencode(123)
+'123'
+>>> urlize(123)
+'123'
+>>> urlizetrunc(123, 1)
+'123'
+>>> wordcount(123)
+1
+>>> wordwrap(123, 2)
+'123'
+>>> ljust('123', 4)
+'123 '
+>>> rjust('123', 4)
+' 123'
+>>> center('123', 5)
+' 123 '
+>>> center('123', 6)
+' 123 '
+>>> cut(123, '2')
+'13'
+>>> escape(123)
+'123'
+>>> linebreaks(123)
+'<p>123</p>'
+>>> linebreaksbr(123)
+'123'
+>>> removetags(123, 'a')
+'123'
+>>> striptags(123)
+'123'
"""
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index c4502b2202..db3969429e 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -318,6 +318,7 @@ The value is compared to its str():
</select>
The 'choices' argument can be any iterable:
+>>> from itertools import chain
>>> def get_choices():
... for i in range(5):
... yield (i, i)
@@ -329,6 +330,17 @@ The 'choices' argument can be any iterable:
<option value="3">3</option>
<option value="4">4</option>
</select>
+>>> things = ({'id': 1, 'name': 'And Boom'}, {'id': 2, 'name': 'One More Thing!'})
+>>> class SomeForm(Form):
+... somechoice = ChoiceField(choices=chain((('', '-'*9),), [(thing['id'], thing['name']) for thing in things]))
+>>> f = SomeForm()
+>>> f.as_table()
+u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="" selected="selected">---------</option>\n<option value="1">And Boom</option>\n<option value="2">One More Thing!</option>\n</select></td></tr>'
+>>> f.as_table()
+u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="" selected="selected">---------</option>\n<option value="1">And Boom</option>\n<option value="2">One More Thing!</option>\n</select></td></tr>'
+>>> f = SomeForm({'somechoice': 2})
+>>> f.as_table()
+u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="">---------</option>\n<option value="1">And Boom</option>\n<option value="2" selected="selected">One More Thing!</option>\n</select></td></tr>'
You can also pass 'choices' to the constructor:
>>> w = Select(choices=[(1, 1), (2, 2), (3, 3)])
@@ -1999,6 +2011,19 @@ For a form with a <select>, use ChoiceField:
<option value="J">Java</option>
</select>
+A subtlety: If one of the choices' value is the empty string and the form is
+unbound, then the <option> for the empty-string choice will get selected="selected".
+>>> class FrameworkForm(Form):
+... name = CharField()
+... language = ChoiceField(choices=[('', '------'), ('P', 'Python'), ('J', 'Java')])
+>>> f = FrameworkForm(auto_id=False)
+>>> print f['language']
+<select name="language">
+<option value="" selected="selected">------</option>
+<option value="P">Python</option>
+<option value="J">Java</option>
+</select>
+
You can specify widget attributes in the Widget constructor.
>>> class FrameworkForm(Form):
... name = CharField()
@@ -3304,6 +3329,75 @@ u''
>>> f.clean('')
u''
+# USPhoneNumberField ##########################################################
+
+USPhoneNumberField validates that the data is a valid U.S. phone number,
+including the area code. It's normalized to XXX-XXX-XXXX format.
+>>> from django.contrib.localflavor.usa.forms import USPhoneNumberField
+>>> f = USPhoneNumberField()
+>>> f.clean('312-555-1212')
+u'312-555-1212'
+>>> f.clean('3125551212')
+u'312-555-1212'
+>>> f.clean('312 555-1212')
+u'312-555-1212'
+>>> f.clean('(312) 555-1212')
+u'312-555-1212'
+>>> f.clean('312 555 1212')
+u'312-555-1212'
+>>> f.clean('312.555.1212')
+u'312-555-1212'
+>>> f.clean('312.555-1212')
+u'312-555-1212'
+>>> f.clean(' (312) 555.1212 ')
+u'312-555-1212'
+>>> f.clean('555-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean('312-55-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> 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 = USPhoneNumberField(required=False)
+>>> f.clean('312-555-1212')
+u'312-555-1212'
+>>> f.clean('3125551212')
+u'312-555-1212'
+>>> f.clean('312 555-1212')
+u'312-555-1212'
+>>> f.clean('(312) 555-1212')
+u'312-555-1212'
+>>> f.clean('312 555 1212')
+u'312-555-1212'
+>>> f.clean('312.555.1212')
+u'312-555-1212'
+>>> f.clean('312.555-1212')
+u'312-555-1212'
+>>> f.clean(' (312) 555.1212 ')
+u'312-555-1212'
+>>> f.clean('555-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean('312-55-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
# USStateField ################################################################
USStateField validates that the data is either an abbreviation or name of a