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 masonsimon+django@gmail.com Manuzhai - Petar Marić + Petar Marić mark@junklight.com Yasushi Masuda mattycakes@gmail.com @@ -166,7 +166,7 @@ answer newbie questions, and generally made Django that much better: smurf@smurf.noris.de sopel Georgi Stanojevski - Thomas Steinacher + Thomas Steinacher nowell strite Radek Švarz Swaroop C H @@ -183,6 +183,7 @@ answer newbie questions, and generally made Django that much better: Milton Waddams wam-djangobug@wamber.net Dan Watson + Chris Wesseling Rachel Willmer Gary Wilson 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 ``&`` 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

and
s" from django.utils.html import linebreaks return linebreaks(value) +linebreaks = stringfilter(linebreaks) def linebreaksbr(value): "Converts newlines into
s" return value.replace('\n', '
') +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.objects.get(id=3) + +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() +

  • Headline:
  • +
  • Pub date:
  • +
  • Writer:
  • +
  • Article:
  • +
  • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
  • +>>> Category.objects.create(name='Fourth', url='4th') + +>>> Writer.objects.create(name='Carl Bernstein') + +>>> print f.as_ul() +
  • Headline:
  • +
  • Pub date:
  • +
  • Writer:
  • +
  • Article:
  • +
  • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
  • + +# 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) + +>>> f.clean(2) + + +# 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') + +>>> f.clean(5) + + +# 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') + +>>> 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]) +[] +>>> f.clean([2]) +[] +>>> f.clean(['1']) +[] +>>> f.clean(['1', '2']) +[, ] +>>> f.clean([1, '2']) +[, ] +>>> f.clean((1, '2')) +[, ] +>>> 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') + +>>> f.clean([6]) +[] + +# 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) +'

    123

    ' +>>> 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(): 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: +>>> 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'' +>>> f.as_table() +u'' +>>> f = SomeForm({'somechoice': 2}) +>>> f.as_table() +u'' 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 +A subtlety: If one of the choices' value is the empty string and the form is +unbound, then the