diff options
| author | Jason Pellerin <jpellerin@gmail.com> | 2006-12-10 15:43:51 +0000 |
|---|---|---|
| committer | Jason Pellerin <jpellerin@gmail.com> | 2006-12-10 15:43:51 +0000 |
| commit | 694f44f600335c30189cbcee73c4d133881c54d2 (patch) | |
| tree | cea7be867425c6d6d09d175f68f2f8dba5aab71c | |
| parent | 55d7f6519c8d13a685cff3f9873b5784fc511a87 (diff) | |
[multi-db] Merged trunk to [4188]. Some tests still failing.
git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@4189 bcc190cf-cafb-0310-a4f2-bffc1f526a37
| -rw-r--r-- | django/contrib/admin/templates/admin/search_form.html | 2 | ||||
| -rw-r--r-- | django/contrib/admin/views/main.py | 14 | ||||
| -rw-r--r-- | django/contrib/contenttypes/management.py | 11 | ||||
| -rw-r--r-- | django/contrib/formtools/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/formtools/preview.py | 160 | ||||
| -rw-r--r-- | django/contrib/formtools/templates/formtools/form.html | 15 | ||||
| -rw-r--r-- | django/contrib/formtools/templates/formtools/preview.html | 36 | ||||
| -rw-r--r-- | django/contrib/sitemaps/__init__.py | 2 | ||||
| -rw-r--r-- | django/core/servers/fastcgi.py | 2 | ||||
| -rw-r--r-- | django/newforms/fields.py | 91 | ||||
| -rw-r--r-- | django/newforms/forms.py | 103 | ||||
| -rw-r--r-- | django/newforms/util.py | 15 | ||||
| -rw-r--r-- | django/newforms/widgets.py | 10 | ||||
| -rw-r--r-- | django/template/__init__.py | 6 | ||||
| -rw-r--r-- | docs/add_ons.txt | 17 | ||||
| -rw-r--r-- | docs/newforms.txt | 79 | ||||
| -rw-r--r-- | docs/settings.txt | 2 | ||||
| -rw-r--r-- | tests/regressiontests/forms/tests.py | 173 | ||||
| -rw-r--r-- | tests/regressiontests/templates/tests.py | 18 |
19 files changed, 600 insertions, 156 deletions
diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html index d9126c3ec5..445cca3089 100644 --- a/django/contrib/admin/templates/admin/search_form.html +++ b/django/contrib/admin/templates/admin/search_form.html @@ -7,7 +7,7 @@ <input type="text" size="40" name="{{ search_var }}" value="{{ cl.query|escape }}" id="searchbar" /> <input type="submit" value="{% trans 'Go' %}" /> {% if show_result_count %} - <span class="small quiet">{% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?">{% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}</a>)</span> + <span class="small quiet">{% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?{% if cl.is_popup %}pop=1{% endif %}">{% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}</a>)</span> {% endif %} {% for pair in cl.params.items %} {% ifnotequal pair.0 search_var %}<input type="hidden" name="{{ pair.0|escape }}" value="{{ pair.1|escape }}"/>{% endifnotequal %} diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 324841a669..c9cff0e374 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -226,7 +226,7 @@ index = staff_member_required(never_cache(index)) def add_stage(request, app_label, model_name, show_delete=False, form_url='', post_url=None, post_url_continue='../%s/', object_id_override=None): model = models.get_model(app_label, model_name) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) opts = model._meta if not request.user.has_perm(app_label + '.' + opts.get_add_permission()): @@ -302,7 +302,7 @@ def change_stage(request, app_label, model_name, object_id): model = models.get_model(app_label, model_name) object_id = unquote(object_id) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) opts = model._meta if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): @@ -313,8 +313,8 @@ def change_stage(request, app_label, model_name, object_id): try: manipulator = model.ChangeManipulator(object_id) - except ObjectDoesNotExist: - raise Http404 + except model.DoesNotExist: + raise Http404('%s object with primary key %r does not exist' % (model_name, escape(object_id))) if request.POST: new_data = request.POST.copy() @@ -490,7 +490,7 @@ def delete_stage(request, app_label, model_name, object_id): model = models.get_model(app_label, model_name) object_id = unquote(object_id) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) opts = model._meta if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()): raise PermissionDenied @@ -527,7 +527,7 @@ def history(request, app_label, model_name, object_id): model = models.get_model(app_label, model_name) object_id = unquote(object_id) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) action_list = LogEntry.objects.filter(object_id=object_id, content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time') # If no history was found, see whether this object even exists. @@ -743,7 +743,7 @@ class ChangeList(object): def change_list(request, app_label, model_name): model = models.get_model(app_label, model_name) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()): raise PermissionDenied try: diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index de3a685477..f492f54303 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -3,9 +3,9 @@ Creates content types for all installed models. """ from django.dispatch import dispatcher -from django.db.models import get_models, signals +from django.db.models import get_apps, get_models, signals -def create_contenttypes(app, created_models, verbosity): +def create_contenttypes(app, created_models, verbosity=2): from django.contrib.contenttypes.models import ContentType app_models = get_models(app) if not app_models: @@ -22,4 +22,11 @@ def create_contenttypes(app, created_models, verbosity): if verbosity >= 2: print "Adding content type '%s | %s'" % (ct.app_label, ct.model) +def create_all_contenttypes(verbosity=2): + for app in get_apps(): + create_contenttypes(app, None, verbosity) + dispatcher.connect(create_contenttypes, signal=signals.post_syncdb) + +if __name__ == "__main__": + create_all_contenttypes() diff --git a/django/contrib/formtools/__init__.py b/django/contrib/formtools/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/formtools/__init__.py diff --git a/django/contrib/formtools/preview.py b/django/contrib/formtools/preview.py new file mode 100644 index 0000000000..9a9371b5f8 --- /dev/null +++ b/django/contrib/formtools/preview.py @@ -0,0 +1,160 @@ +""" +Formtools Preview application. + +This is an abstraction of the following workflow: + + "Display an HTML form, force a preview, then do something with the submission." + +Given a django.newforms.Form object that you define, this takes care of the +following: + + * Displays the form as HTML on a Web page. + * Validates the form data once it's submitted via POST. + * If it's valid, displays a preview page. + * If it's not valid, redisplays the form with error messages. + * At the preview page, if the preview confirmation button is pressed, calls + a hook that you define -- a done() method. + +The framework enforces the required preview by passing a shared-secret hash to +the preview page. If somebody tweaks the form parameters on the preview page, +the form submission will fail the hash comparison test. + +Usage +===== + +Subclass FormPreview and define a done() method: + + def done(self, request, clean_data): + # ... + +This method takes an HttpRequest object and a dictionary of the form data after +it has been validated and cleaned. It should return an HttpResponseRedirect. + +Then, just instantiate your FormPreview subclass by passing it a Form class, +and pass that to your URLconf, like so: + + (r'^post/$', MyFormPreview(MyForm)), + +The FormPreview class has a few other hooks. See the docstrings in the source +code below. + +The framework also uses two templates: 'formtools/preview.html' and +'formtools/form.html'. You can override these by setting 'preview_template' and +'form_template' attributes on your FormPreview subclass. See +django/contrib/formtools/templates for the default templates. +""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404 +from django.shortcuts import render_to_response +import cPickle as pickle +import md5 + +AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. + +class FormPreview(object): + preview_template = 'formtools/preview.html' + form_template = 'formtools/form.html' + + # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### + + def __init__(self, form): + # form should be a Form class, not an instance. + self.form, self.state = form, {} + + def __call__(self, request, *args, **kwargs): + stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview') + self.parse_params(*args, **kwargs) + try: + method = getattr(self, stage + '_' + request.method.lower()) + except AttributeError: + raise Http404 + return method(request) + + def unused_name(self, name): + """ + Given a first-choice name, adds an underscore to the name until it + reaches a name that isn't claimed by any field in the form. + + This is calculated rather than being hard-coded so that no field names + are off-limits for use in the form. + """ + while 1: + try: + f = self.form.fields[name] + except KeyError: + break # This field name isn't being used by the form. + name += '_' + return name + + def preview_get(self, request): + "Displays the form" + f = self.form(auto_id=AUTO_ID) + return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}) + + def preview_post(self, request): + "Validates the POST data. If valid, displays the preview page. Else, redisplays form." + f = self.form(request.POST, auto_id=AUTO_ID) + context = {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state} + if f.is_valid(): + context['hash_field'] = self.unused_name('hash') + context['hash_value'] = self.security_hash(request, f) + return render_to_response(self.preview_template, context) + else: + return render_to_response(self.form_template, context) + + def post_post(self, request): + "Validates the POST data. If valid, calls done(). Else, redisplays form." + f = self.form(request.POST, auto_id=AUTO_ID) + if f.is_valid(): + if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')): + return self.failed_hash(request) # Security hash failed. + return self.done(request, f.clean_data) + else: + return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}) + + # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## + + def parse_params(self, *args, **kwargs): + """ + Given captured args and kwargs from the URLconf, saves something in + self.state and/or raises Http404 if necessary. + + For example, this URLconf captures a user_id variable: + + (r'^contact/(?P<user_id>\d{1,6})/$', MyFormPreview(MyForm)), + + In this case, the kwargs variable in parse_params would be + {'user_id': 32} for a request to '/contact/32/'. You can use that + user_id to make sure it's a valid user and/or save it for later, for + use in done(). + """ + pass + + def security_hash(self, request, form): + """ + Calculates the security hash for the given Form instance. + + This creates a list of the form field names/values in a deterministic + order, pickles the result with the SECRET_KEY setting and takes an md5 + hash of that. + + Subclasses may want to take into account request-specific information + such as the IP address. + """ + data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY] + # Use HIGHEST_PROTOCOL because it's the most efficient. It requires + # Python 2.3, but Django requires 2.3 anyway, so that's OK. + pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL) + return md5.new(pickled).hexdigest() + + def failed_hash(self, request): + "Returns an HttpResponse in the case of an invalid security hash." + return self.preview_post(request) + + # METHODS SUBCLASSES MUST OVERRIDE ######################################## + + def done(self, request, clean_data): + "Does something with the clean_data and returns an HttpResponseRedirect." + raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__) diff --git a/django/contrib/formtools/templates/formtools/form.html b/django/contrib/formtools/templates/formtools/form.html new file mode 100644 index 0000000000..90da8b2b2b --- /dev/null +++ b/django/contrib/formtools/templates/formtools/form.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block content %} + +{% if form.errors %}<h1>Please correct the following errors</h1>{% else %}<h1>Submit</h1>{% endif %} + +<form action="" method="post"> +<table> +{{ form }} +</table> +<input type="hidden" name="{{ stage_field }}" value="1" /> +<p><input type="submit" value="Submit" /></p> +</form> + +{% endblock %} diff --git a/django/contrib/formtools/templates/formtools/preview.html b/django/contrib/formtools/templates/formtools/preview.html new file mode 100644 index 0000000000..c7955d46e1 --- /dev/null +++ b/django/contrib/formtools/templates/formtools/preview.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} + +<h1>Preview your submission</h1> + +<table> +{% for field in form %} +<tr> +<th>{{ field.verbose_name }}:</th> +<td>{{ field.data|escape }}</td> +</tr> +{% endfor %} +</table> + +<p>Security hash: {{ hash_value }}</p> + +<form action="" method="post"> +{% for field in form %}{{ field.as_hidden }} +{% endfor %} +<input type="hidden" name="{{ stage_field }}" value="2" /> +<input type="hidden" name="{{ hash_field }}" value="{{ hash_value }}" /> +<p><input type="submit" value="Submit" /></p> +</form> + +<h1>Or edit it again</h1> + +<form action="" method="post"> +<table> +{{ form }} +</table> +<input type="hidden" name="{{ stage_field }}" value="1" /> +<p><input type="submit" value="Submit changes" /></p> +</form> + +{% endblock %} diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 2c76e13c22..44ede4460a 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -29,7 +29,7 @@ def ping_google(sitemap_url=None, ping_url=PING_URL): from django.contrib.sites.models import Site current_site = Site.objects.get_current() - url = "%s%s" % (current_site.domain, sitemap) + url = "%s%s" % (current_site.domain, sitemap_url) params = urllib.urlencode({'sitemap':url}) urllib.urlopen("%s?%s" % (ping_url, params)) diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py index fccb7bf087..649dd6942d 100644 --- a/django/core/servers/fastcgi.py +++ b/django/core/servers/fastcgi.py @@ -118,6 +118,8 @@ def runfastcgi(argset=[], **kwargs): else: return fastcgi_help("ERROR: Implementation must be one of prefork or thread.") + wsgi_opts['debug'] = False # Turn off flup tracebacks + # Prep up and go from django.core.handlers.wsgi import WSGIHandler diff --git a/django/newforms/fields.py b/django/newforms/fields.py index b3d44c24ae..ce92886371 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -2,8 +2,9 @@ Field classes """ -from util import ValidationError, DEFAULT_ENCODING, smart_unicode -from widgets import TextInput, CheckboxInput, Select, SelectMultiple +from django.utils.translation import gettext +from util import ValidationError, smart_unicode +from widgets import TextInput, PasswordInput, CheckboxInput, Select, SelectMultiple import datetime import re import time @@ -31,11 +32,17 @@ class Field(object): # Tracks each time a Field instance is created. Used to retain order. creation_counter = 0 - def __init__(self, required=True, widget=None): - self.required = required + def __init__(self, required=True, widget=None, label=None): + self.required, self.label = required, label widget = widget or self.widget if isinstance(widget, type): widget = widget() + + # Hook into self.widget_attrs() for any Field-specific HTML attributes. + extra_attrs = self.widget_attrs(widget) + if extra_attrs: + widget.attrs.update(extra_attrs) + self.widget = widget # Increase the creation counter, and save our local copy. @@ -50,13 +57,21 @@ class Field(object): Raises ValidationError for any errors. """ if self.required and value in EMPTY_VALUES: - raise ValidationError(u'This field is required.') + raise ValidationError(gettext(u'This field is required.')) return value + def widget_attrs(self, widget): + """ + Given a Widget instance (*not* a Widget class), returns a dictionary of + any HTML attributes that should be added to the Widget, based on this + Field. + """ + return {} + class CharField(Field): - def __init__(self, max_length=None, min_length=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): self.max_length, self.min_length = max_length, min_length + Field.__init__(self, required, widget, label) def clean(self, value): "Validates max_length and min_length. Returns a Unicode object." @@ -64,11 +79,15 @@ class CharField(Field): if value in EMPTY_VALUES: value = u'' value = smart_unicode(value) if self.max_length is not None and len(value) > self.max_length: - raise ValidationError(u'Ensure this value has at most %d characters.' % self.max_length) + raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) if self.min_length is not None and len(value) < self.min_length: - raise ValidationError(u'Ensure this value has at least %d characters.' % self.min_length) + raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) return value + def widget_attrs(self, widget): + if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + return {'maxlength': str(self.max_length)} + class IntegerField(Field): def clean(self, value): """ @@ -81,7 +100,7 @@ class IntegerField(Field): try: return int(value) except (ValueError, TypeError): - raise ValidationError(u'Enter a whole number.') + raise ValidationError(gettext(u'Enter a whole number.')) DEFAULT_DATE_INPUT_FORMATS = ( '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' @@ -92,8 +111,8 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) class DateField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS def clean(self, value): @@ -113,7 +132,7 @@ class DateField(Field): return datetime.date(*time.strptime(value, format)[:3]) except ValueError: continue - raise ValidationError(u'Enter a valid date.') + raise ValidationError(gettext(u'Enter a valid date.')) DEFAULT_DATETIME_INPUT_FORMATS = ( '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' @@ -128,8 +147,8 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( ) class DateTimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS def clean(self, value): @@ -149,20 +168,20 @@ class DateTimeField(Field): return datetime.datetime(*time.strptime(value, format)[:6]) except ValueError: continue - raise ValidationError(u'Enter a valid date/time.') + raise ValidationError(gettext(u'Enter a valid date/time.')) class RegexField(Field): - def __init__(self, regex, error_message=None, required=True, widget=None): + def __init__(self, regex, error_message=None, required=True, widget=None, label=None): """ regex can be either a string or a compiled regular expression object. error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ - Field.__init__(self, required, widget) + Field.__init__(self, required, widget, label) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex - self.error_message = error_message or u'Enter a valid value.' + self.error_message = error_message or gettext(u'Enter a valid value.') def clean(self, value): """ @@ -184,8 +203,8 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): - def __init__(self, required=True, widget=None): - RegexField.__init__(self, email_re, u'Enter a valid e-mail address.', required, widget) + def __init__(self, required=True, widget=None, label=None): + RegexField.__init__(self, email_re, gettext(u'Enter a valid e-mail address.'), required, widget, label) url_re = re.compile( r'^https?://' # http:// or https:// @@ -201,9 +220,9 @@ except ImportError: URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' class URLField(RegexField): - def __init__(self, required=True, verify_exists=False, widget=None, + def __init__(self, required=True, verify_exists=False, widget=None, label=None, validator_user_agent=URL_VALIDATOR_USER_AGENT): - RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget) + RegexField.__init__(self, url_re, gettext(u'Enter a valid URL.'), required, widget, label) self.verify_exists = verify_exists self.user_agent = validator_user_agent @@ -223,9 +242,9 @@ class URLField(RegexField): req = urllib2.Request(value, None, headers) u = urllib2.urlopen(req) except ValueError: - raise ValidationError(u'Enter a valid URL.') + raise ValidationError(gettext(u'Enter a valid URL.')) except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError(u'This URL appears to be a broken link.') + raise ValidationError(gettext(u'This URL appears to be a broken link.')) return value class BooleanField(Field): @@ -237,10 +256,10 @@ class BooleanField(Field): return bool(value) class ChoiceField(Field): - def __init__(self, choices=(), required=True, widget=Select): + def __init__(self, choices=(), required=True, widget=Select, label=None): if isinstance(widget, type): widget = widget(choices=choices) - Field.__init__(self, required, widget) + Field.__init__(self, required, widget, label) self.choices = choices def clean(self, value): @@ -254,37 +273,37 @@ class ChoiceField(Field): return value valid_values = set([str(k) for k, v in self.choices]) if value not in valid_values: - raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % value) + raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % value) return value class MultipleChoiceField(ChoiceField): - def __init__(self, choices=(), required=True, widget=SelectMultiple): - ChoiceField.__init__(self, choices, required, widget) + def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None): + ChoiceField.__init__(self, choices, required, widget, label) def clean(self, value): """ Validates that the input is a list or tuple. """ if self.required and not value: - raise ValidationError(u'This field is required.') + raise ValidationError(gettext(u'This field is required.')) elif not self.required and not value: return [] if not isinstance(value, (list, tuple)): - raise ValidationError(u'Enter a list of values.') + raise ValidationError(gettext(u'Enter a list of values.')) new_value = [] for val in value: val = smart_unicode(val) new_value.append(val) # Validate that each value in the value list is in self.choices. - valid_values = set([k for k, v in self.choices]) + valid_values = set([smart_unicode(k) for k, v in self.choices]) for val in new_value: if val not in valid_values: - raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % val) + raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val) return new_value class ComboField(Field): - def __init__(self, fields=(), required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, fields=(), required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) # Set 'required' to False on the individual fields, because the # required validation will be handled by ComboField, not by those # individual fields. diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 4bc6173249..9f855dc4f5 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -6,7 +6,7 @@ from django.utils.datastructures import SortedDict from django.utils.html import escape from fields import Field from widgets import TextInput, Textarea, HiddenInput -from util import ErrorDict, ErrorList, ValidationError +from util import StrAndUnicode, ErrorDict, ErrorList, ValidationError NON_FIELD_ERRORS = '__all__' @@ -32,7 +32,7 @@ class DeclarativeFieldsMetaclass(type): attrs['fields'] = SortedDictFromList(fields) return type.__new__(cls, name, bases, attrs) -class Form(object): +class Form(StrAndUnicode): "A collection of Fields, plus their associated data." __metaclass__ = DeclarativeFieldsMetaclass @@ -43,7 +43,7 @@ class Form(object): self.clean_data = None # Stores the data after clean() has been called. self.__errors = None # Stores the errors after clean() has been called. - def __str__(self): + def __unicode__(self): return self.as_table() def __iter__(self): @@ -72,41 +72,44 @@ class Form(object): """ return not self.ignore_errors and not bool(self.errors) - def as_table(self): - "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." - output = [] - if self.errors.get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append(u'<tr><td colspan="2">%s</td></tr>' % self.non_field_errors()) + def _html_output(self, normal_row, error_row, row_ender, errors_on_separate_row): + "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." + top_errors = self.non_field_errors() # Errors that should be displayed above all fields. + output, hidden_fields = [], [] for name, field in self.fields.items(): bf = BoundField(self, field, name) + bf_errors = bf.errors # Cache in local variable. if bf.is_hidden: - if bf.errors: - new_errors = ErrorList(['(Hidden field %s) %s' % (name, e) for e in bf.errors]) - output.append(u'<tr><td colspan="2">%s</td></tr>' % new_errors) - output.append(str(bf)) + if bf_errors: + top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors]) + hidden_fields.append(unicode(bf)) else: - if bf.errors: - output.append(u'<tr><td colspan="2">%s</td></tr>' % bf.errors) - output.append(u'<tr><td>%s</td><td>%s</td></tr>' % (bf.label_tag(escape(bf.verbose_name+':')), bf)) + if errors_on_separate_row and bf_errors: + output.append(error_row % bf_errors) + output.append(normal_row % {'errors': bf_errors, 'label': bf.label_tag(escape(bf.label+':')), 'field': bf}) + if top_errors: + output.insert(0, error_row % top_errors) + if hidden_fields: # Insert any hidden fields in the last row. + str_hidden = u''.join(hidden_fields) + if output: + last_row = output[-1] + # Chop off the trailing row_ender (e.g. '</td></tr>') and insert the hidden fields. + output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender + else: # If there aren't any rows in the output, just append the hidden fields. + output.append(str_hidden) return u'\n'.join(output) + def as_table(self): + "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." + return self._html_output(u'<tr><td>%(label)s</td><td>%(field)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', True) + def as_ul(self): "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." - output = [] - if self.errors.get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append(u'<li>%s</li>' % self.non_field_errors()) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - if bf.is_hidden: - if bf.errors: - new_errors = ErrorList(['(Hidden field %s) %s' % (name, e) for e in bf.errors]) - output.append(u'<li>%s</li>' % new_errors) - output.append(str(bf)) - else: - output.append(u'<li>%s%s %s</li>' % (bf.errors, bf.label_tag(escape(bf.verbose_name+':')), bf)) - return u'\n'.join(output) + return self._html_output(u'<li>%(errors)s%(label)s %(field)s</li>', u'<li>%s</li>', '</li>', False) + + def as_p(self): + "Returns this form rendered as HTML <p>s." + return self._html_output(u'<p>%(label)s %(field)s</p>', u'<p>%s</p>', '</p>', True) def non_field_errors(self): """ @@ -155,18 +158,19 @@ class Form(object): """ return self.clean_data -class BoundField(object): +class BoundField(StrAndUnicode): "A Field plus data" def __init__(self, form, field, name): - self._form = form - self._field = field - self._name = name + self.form = form + self.field = field + self.name = name + self.label = self.field.label or pretty_name(name) - def __str__(self): + def __unicode__(self): "Renders this field as an HTML widget." # Use the 'widget' attribute on the field to determine which type # of HTML widget to use. - value = self.as_widget(self._field.widget) + value = self.as_widget(self.field.widget) if not isinstance(value, basestring): # Some Widget render() methods -- notably RadioSelect -- return a # "special" object rather than a string. Call the __str__() on that @@ -179,10 +183,7 @@ class BoundField(object): Returns an ErrorList for this field. Returns an empty ErrorList if there are none. """ - try: - return self._form.errors[self._name] - except KeyError: - return ErrorList() + return self.form.errors.get(self.name, ErrorList()) errors = property(_errors) def as_widget(self, widget, attrs=None): @@ -190,7 +191,7 @@ class BoundField(object): auto_id = self.auto_id if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'): attrs['id'] = auto_id - return widget.render(self._name, self.data, attrs=attrs) + return widget.render(self.name, self.data, attrs=attrs) def as_text(self, attrs=None): """ @@ -210,21 +211,17 @@ class BoundField(object): def _data(self): "Returns the data for this BoundField, or None if it wasn't given." - return self._form.data.get(self._name, None) + return self.form.data.get(self.name, None) data = property(_data) - def _verbose_name(self): - return pretty_name(self._name) - verbose_name = property(_verbose_name) - def label_tag(self, contents=None): """ Wraps the given contents in a <label>, if the field has an ID attribute. Does not HTML-escape the contents. If contents aren't given, uses the - field's HTML-escaped verbose_name. + field's HTML-escaped label. """ - contents = contents or escape(self.verbose_name) - widget = self._field.widget + contents = contents or escape(self.label) + widget = self.field.widget id_ = widget.attrs.get('id') or self.auto_id if id_: contents = '<label for="%s">%s</label>' % (widget.id_for_label(id_), contents) @@ -232,7 +229,7 @@ class BoundField(object): def _is_hidden(self): "Returns True if this BoundField's widget is hidden." - return self._field.widget.is_hidden + return self.field.widget.is_hidden is_hidden = property(_is_hidden) def _auto_id(self): @@ -240,10 +237,10 @@ class BoundField(object): Calculates and returns the ID attribute for this BoundField, if the associated Form has specified auto_id. Returns an empty string otherwise. """ - auto_id = self._form.auto_id + auto_id = self.form.auto_id if auto_id and '%s' in str(auto_id): - return str(auto_id) % self._name + return str(auto_id) % self.name elif auto_id: - return self._name + return self.name return '' auto_id = property(_auto_id) diff --git a/django/newforms/util.py b/django/newforms/util.py index a5cc4932ea..a78623a17b 100644 --- a/django/newforms/util.py +++ b/django/newforms/util.py @@ -1,13 +1,22 @@ -# Default encoding for input byte strings. -DEFAULT_ENCODING = 'utf-8' # TODO: First look at django.conf.settings, then fall back to this. +from django.conf import settings def smart_unicode(s): if not isinstance(s, basestring): s = unicode(str(s)) elif not isinstance(s, unicode): - s = unicode(s, DEFAULT_ENCODING) + s = unicode(s, settings.DEFAULT_CHARSET) return s +class StrAndUnicode(object): + """ + A class whose __str__ returns its __unicode__ as a bytestring + according to settings.DEFAULT_CHARSET. + + Useful as a mix-in. + """ + def __str__(self): + return self.__unicode__().encode(settings.DEFAULT_CHARSET) + class ErrorDict(dict): """ A collection of errors that knows how to display itself in various formats. diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index 274ba01225..1a2d7d67c9 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -8,7 +8,7 @@ __all__ = ( 'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', ) -from util import smart_unicode +from util import StrAndUnicode, smart_unicode from django.utils.html import escape from itertools import chain @@ -146,7 +146,7 @@ class SelectMultiple(Widget): output.append(u'</select>') return u'\n'.join(output) -class RadioInput(object): +class RadioInput(StrAndUnicode): "An object used by RadioFieldRenderer that represents a single <input type='radio'>." def __init__(self, name, value, attrs, choice, index): self.name, self.value = name, value @@ -154,7 +154,7 @@ class RadioInput(object): self.choice_value, self.choice_label = choice self.index = index - def __str__(self): + def __unicode__(self): return u'<label>%s %s</label>' % (self.tag(), self.choice_label) def is_checked(self): @@ -168,7 +168,7 @@ class RadioInput(object): final_attrs['checked'] = 'checked' return u'<input%s />' % flatatt(final_attrs) -class RadioFieldRenderer(object): +class RadioFieldRenderer(StrAndUnicode): "An object used by RadioSelect to enable customization of radio widgets." def __init__(self, name, value, attrs, choices): self.name, self.value, self.attrs = name, value, attrs @@ -178,7 +178,7 @@ class RadioFieldRenderer(object): for i, choice in enumerate(self.choices): yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i) - def __str__(self): + def __unicode__(self): "Outputs a <ul> for this set of radio fields." return u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' % w for w in self]) diff --git a/django/template/__init__.py b/django/template/__init__.py index 5affafeba9..7718801684 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -742,7 +742,11 @@ class VariableNode(Node): def encode_output(self, output): # Check type so that we don't run str() on a Unicode object if not isinstance(output, basestring): - return str(output) + try: + return str(output) + except UnicodeEncodeError: + # If __str__() returns a Unicode object, convert it to bytestring. + return unicode(output).encode(settings.DEFAULT_CHARSET) elif isinstance(output, unicode): return output.encode(settings.DEFAULT_CHARSET) else: diff --git a/docs/add_ons.txt b/docs/add_ons.txt index a0377700d7..58c01c4fc0 100644 --- a/docs/add_ons.txt +++ b/docs/add_ons.txt @@ -48,6 +48,23 @@ See the `csrf documentation`_. .. _csrf documentation: http://www.djangoproject.com/documentation/csrf/ +formtools +========= + +**New in Django development version** + +A set of high-level abstractions for Django forms (django.newforms). + +django.contrib.formtools.preview +-------------------------------- + +An abstraction of the following workflow: + +"Display an HTML form, force a preview, then do something with the submission." + +Full documentation for this feature does not yet exist, but you can read the +code and docstrings in ``django/contrib/formtools/preview.py`` for a start. + humanize ======== diff --git a/docs/newforms.txt b/docs/newforms.txt new file mode 100644 index 0000000000..9bfbc75ee7 --- /dev/null +++ b/docs/newforms.txt @@ -0,0 +1,79 @@ +==================== +The newforms library +==================== + +``django.newforms`` is a new replacement for ``django.forms``, the old Django +form/manipulator/validation framework. This document explains how to use this +new form library. + +Migration plan +============== + +``django.newforms`` currently is only available in the Django development version +-- i.e., it's not available in the Django 0.95 release. For the next Django +release, our plan is to do the following: + + * Move the current ``django.forms`` to ``django.oldforms``. This will allow + for an eased migration of form code. You'll just have to change your + import statements:: + + from django import forms # old + from django import oldforms as forms # new + + * Move the current ``django.newforms`` to ``django.forms``. + + * We will remove ``django.oldforms`` in the release *after* the next Django + release -- the release that comes after the release in which we're + creating ``django.oldforms``. + +With this in mind, we recommend you use the following import statement when +using ``django.newforms``:: + + from django import newforms as forms + +This way, your code can refer to the ``forms`` module, and when +``django.newforms`` is renamed to ``django.forms``, you'll only have to change +your ``import`` statements. + +If you prefer "``import *``" syntax, you can do the following:: + + from django.newforms import * + +This will import all fields, widgets, form classes and other various utilities +into your local namespace. Some people find this convenient; others find it +too messy. The choice is yours. + +Overview +======== + +As the ``django.forms`` system before it, ``django.newforms`` is intended to +handle HTML form display, validation and redisplay. It's what you use if you +want to perform server-side validation for an HTML form. + +The library deals with these concepts: + + * **Widget** -- A class that corresponds to an HTML form widget, e.g. + ``<input type="text">`` or ``<textarea>``. This handles rendering of the + widget as HTML. + + * **Field** -- A class that is responsible for doing validation, e.g. + an ``EmailField`` that makes sure its data is a valid e-mail address. + + * **Form** -- A collection of fields that knows how to validate itself and + display itself as HTML. + + + +Using forms with templates +========================== + +Using forms in views +==================== + +More coming soon +================ + +That's all the documentation for now. For more, see the file +http://code.djangoproject.com/browser/django/trunk/tests/regressiontests/forms/tests.py +-- the unit tests for ``django.newforms``. This can give you a good idea of +what's possible. diff --git a/docs/settings.txt b/docs/settings.txt index 61a8f7622a..a29812c098 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -837,7 +837,7 @@ Default: ``Django/<version> (http://www.djangoproject.com/)`` The string to use as the ``User-Agent`` header when checking to see if URLs exist (see the ``verify_exists`` option on URLField_). -.. URLField: ../model_api/#urlfield +.. _URLField: ../model_api/#urlfield USE_ETAGS --------- diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index bc38154d74..e362dee671 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -636,6 +636,9 @@ Each Field's __init__() takes at least these parameters: 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. Other than that, the Field subclasses have class-specific options for __init__(). For example, CharField has a max_length option. @@ -1335,7 +1338,7 @@ u'' <input type="text" name="last_name" value="Lennon" /> <input type="text" name="birthday" value="1940-10-9" /> >>> for boundfield in p: -... print boundfield.verbose_name, boundfield.data +... print boundfield.label, boundfield.data First name John Last name Lennon Birthday 1940-10-9 @@ -1368,6 +1371,13 @@ False <li><ul class="errorlist"><li>This field is required.</li></ul>First name: <input type="text" name="first_name" /></li> <li><ul class="errorlist"><li>This field is required.</li></ul>Last name: <input type="text" name="last_name" /></li> <li><ul class="errorlist"><li>This field is required.</li></ul>Birthday: <input type="text" name="birthday" /></li> +>>> print p.as_p() +<p><ul class="errorlist"><li>This field is required.</li></ul></p> +<p>First name: <input type="text" name="first_name" /></p> +<p><ul class="errorlist"><li>This field is required.</li></ul></p> +<p>Last name: <input type="text" name="last_name" /></p> +<p><ul class="errorlist"><li>This field is required.</li></ul></p> +<p>Birthday: <input type="text" name="birthday" /></p> If you don't pass any values to the Form's __init__(), or if you pass None, the Form won't do any validation. Form.errors will be an empty dictionary *but* @@ -1389,6 +1399,10 @@ False <li>First name: <input type="text" name="first_name" /></li> <li>Last name: <input type="text" name="last_name" /></li> <li>Birthday: <input type="text" name="birthday" /></li> +>>> print p.as_p() +<p>First name: <input type="text" name="first_name" /></p> +<p>Last name: <input type="text" name="last_name" /></p> +<p>Birthday: <input type="text" name="birthday" /></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'}) @@ -1396,6 +1410,8 @@ Unicode values are handled properly. u'<tr><td>First name:</td><td><input type="text" name="first_name" value="John" /></td></tr>\n<tr><td>Last name:</td><td><input type="text" name="last_name" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /></td></tr>\n<tr><td>Birthday:</td><td><input type="text" name="birthday" value="1940-10-9" /></td></tr>' >>> p.as_ul() u'<li>First name: <input type="text" name="first_name" value="John" /></li>\n<li>Last name: <input type="text" name="last_name" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /></li>\n<li>Birthday: <input type="text" name="birthday" value="1940-10-9" /></li>' +>>> p.as_p() +u'<p>First name: <input type="text" name="first_name" value="John" /></p>\n<p>Last name: <input type="text" name="last_name" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /></p>\n<p>Birthday: <input type="text" name="birthday" value="1940-10-9" /></p>' >>> p = Person({'last_name': u'Lennon'}) >>> p.errors @@ -1432,14 +1448,18 @@ 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 <label> around the human-readable labels for a field. >>> p = Person(auto_id='id_%s') ->>> print p.as_ul() -<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></li> -<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li> -<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></li> >>> print p.as_table() <tr><td><label for="id_first_name">First name:</label></td><td><input type="text" name="first_name" id="id_first_name" /></td></tr> <tr><td><label for="id_last_name">Last name:</label></td><td><input type="text" name="last_name" id="id_last_name" /></td></tr> <tr><td><label for="id_birthday">Birthday:</label></td><td><input type="text" name="birthday" id="id_birthday" /></td></tr> +>>> print p.as_ul() +<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></li> +<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li> +<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></li> +>>> print p.as_p() +<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p> +<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p> +<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></p> If auto_id is any True value whose str() does not contain '%s', the "id" attribute will be the name of the field. @@ -1596,6 +1616,12 @@ ID of the *first* radio button. <li><label><input type="radio" id="id_language_0" value="P" name="language" /> Python</label></li> <li><label><input type="radio" id="id_language_1" value="J" name="language" /> Java</label></li> </ul></li> +>>> print f.as_p() +<p><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></p> +<p><label for="id_language_0">Language:</label> <ul> +<li><label><input type="radio" id="id_language_0" value="P" name="language" /> Python</label></li> +<li><label><input type="radio" id="id_language_1" value="J" name="language" /> Java</label></li> +</ul></p> MultipleChoiceField is a special case, as its data is required to be a list: >>> class SongForm(Form): @@ -1713,7 +1739,7 @@ Form.clean() is required to return a dictionary of all clean data. >>> f = UserRegistration({}) >>> print f.as_table() <tr><td colspan="2"><ul class="errorlist"><li>This field is required.</li></ul></td></tr> -<tr><td>Username:</td><td><input type="text" name="username" /></td></tr> +<tr><td>Username:</td><td><input type="text" name="username" maxlength="10" /></td></tr> <tr><td colspan="2"><ul class="errorlist"><li>This field is required.</li></ul></td></tr> <tr><td>Password1:</td><td><input type="password" name="password1" /></td></tr> <tr><td colspan="2"><ul class="errorlist"><li>This field is required.</li></ul></td></tr> @@ -1725,12 +1751,12 @@ Form.clean() is required to return a dictionary of all clean data. {'__all__': [u'Please make sure your passwords match.']} >>> print f.as_table() <tr><td colspan="2"><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></td></tr> -<tr><td>Username:</td><td><input type="text" name="username" value="adrian" /></td></tr> +<tr><td>Username:</td><td><input type="text" name="username" value="adrian" maxlength="10" /></td></tr> <tr><td>Password1:</td><td><input type="password" name="password1" value="foo" /></td></tr> <tr><td>Password2:</td><td><input type="password" name="password2" value="bar" /></td></tr> >>> print f.as_ul() <li><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></li> -<li>Username: <input type="text" name="username" value="adrian" /></li> +<li>Username: <input type="text" name="username" value="adrian" maxlength="10" /></li> <li>Password1: <input type="password" name="password1" value="foo" /></li> <li>Password2: <input type="password" name="password2" value="bar" /></li> >>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'}) @@ -1754,9 +1780,10 @@ subclass' __init__(). <tr><td>Last name:</td><td><input type="text" name="last_name" /></td></tr> <tr><td>Birthday:</td><td><input type="text" name="birthday" /></td></tr> -HiddenInput widgets are displayed differently in the as_table() and as_ul() -output of a Form -- their verbose names are not displayed, and a separate -<tr>/<li> is not displayed. +HiddenInput widgets are displayed differently in the as_table(), as_ul() +and as_p() output of a Form -- their verbose names are not displayed, and a +separate row is not displayed. They're displayed in the last row of the +form, directly after that row's form element. >>> class Person(Form): ... first_name = CharField() ... last_name = CharField() @@ -1766,43 +1793,63 @@ output of a Form -- their verbose names are not displayed, and a separate >>> print p <tr><td>First name:</td><td><input type="text" name="first_name" /></td></tr> <tr><td>Last name:</td><td><input type="text" name="last_name" /></td></tr> -<input type="hidden" name="hidden_text" /> -<tr><td>Birthday:</td><td><input type="text" name="birthday" /></td></tr> +<tr><td>Birthday:</td><td><input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></td></tr> >>> print p.as_ul() <li>First name: <input type="text" name="first_name" /></li> <li>Last name: <input type="text" name="last_name" /></li> -<input type="hidden" name="hidden_text" /> -<li>Birthday: <input type="text" name="birthday" /></li> +<li>Birthday: <input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></li> +>>> print p.as_p() +<p>First name: <input type="text" name="first_name" /></p> +<p>Last name: <input type="text" name="last_name" /></p> +<p>Birthday: <input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></p> With auto_id set, a HiddenInput still gets an ID, but it doesn't get a label. >>> p = Person(auto_id='id_%s') >>> print p <tr><td><label for="id_first_name">First name:</label></td><td><input type="text" name="first_name" id="id_first_name" /></td></tr> <tr><td><label for="id_last_name">Last name:</label></td><td><input type="text" name="last_name" id="id_last_name" /></td></tr> -<input type="hidden" name="hidden_text" id="id_hidden_text" /> -<tr><td><label for="id_birthday">Birthday:</label></td><td><input type="text" name="birthday" id="id_birthday" /></td></tr> +<tr><td><label for="id_birthday">Birthday:</label></td><td><input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></td></tr> >>> print p.as_ul() <li><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></li> <li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li> -<input type="hidden" name="hidden_text" id="id_hidden_text" /> -<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></li> +<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></li> +>>> print p.as_p() +<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p> +<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p> +<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></p> If a field with a HiddenInput has errors, the as_table() and as_ul() output will include the error message(s) with the text "(Hidden field [fieldname]) " -prepended. +prepended. This message is displayed at the top of the output, regardless of +its field's order in the form. >>> p = Person({'first_name': 'John', 'last_name': 'Lennon', 'birthday': '1940-10-9'}) >>> print p +<tr><td colspan="2"><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></td></tr> <tr><td>First name:</td><td><input type="text" name="first_name" value="John" /></td></tr> <tr><td>Last name:</td><td><input type="text" name="last_name" value="Lennon" /></td></tr> -<tr><td colspan="2"><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></td></tr> -<input type="hidden" name="hidden_text" /> -<tr><td>Birthday:</td><td><input type="text" name="birthday" value="1940-10-9" /></td></tr> +<tr><td>Birthday:</td><td><input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></td></tr> >>> print p.as_ul() +<li><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></li> <li>First name: <input type="text" name="first_name" value="John" /></li> <li>Last name: <input type="text" name="last_name" value="Lennon" /></li> -<li><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></li> -<input type="hidden" name="hidden_text" /> -<li>Birthday: <input type="text" name="birthday" value="1940-10-9" /></li> +<li>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></li> +>>> print p.as_p() +<p><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></p> +<p>First name: <input type="text" name="first_name" value="John" /></p> +<p>Last name: <input type="text" name="last_name" value="Lennon" /></p> +<p>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></p> + +A corner case: It's possible for a form to have only HiddenInputs. +>>> class TestForm(Form): +... foo = CharField(widget=HiddenInput) +... bar = CharField(widget=HiddenInput) +>>> p = TestForm() +>>> print p.as_table() +<input type="hidden" name="foo" /><input type="hidden" name="bar" /> +>>> print p.as_ul() +<input type="hidden" name="foo" /><input type="hidden" name="bar" /> +>>> print p.as_p() +<input type="hidden" name="foo" /><input type="hidden" name="bar" /> A Form's fields are displayed in the same order in which they were defined. >>> class TestForm(Form): @@ -1837,6 +1884,46 @@ A Form's fields are displayed in the same order in which they were defined. <tr><td>Field13:</td><td><input type="text" name="field13" /></td></tr> <tr><td>Field14:</td><td><input type="text" name="field14" /></td></tr> +Some Field classes have an effect on the HTML attributes of their associated +Widget. If you set max_length in a CharField and its associated widget is +either a TextInput or PasswordInput, then the widget's rendered HTML will +include the "maxlength" attribute. +>>> class UserRegistration(Form): +... username = CharField(max_length=10) # uses TextInput by default +... password = CharField(max_length=10, widget=PasswordInput) +... realname = CharField(max_length=10, widget=TextInput) # redundantly define widget, just to test +... address = CharField() # no max_length defined here +>>> p = UserRegistration() +>>> print p.as_ul() +<li>Username: <input type="text" name="username" maxlength="10" /></li> +<li>Password: <input type="password" name="password" maxlength="10" /></li> +<li>Realname: <input type="text" name="realname" maxlength="10" /></li> +<li>Address: <input type="text" name="address" /></li> + +If you specify a custom "attrs" that includes the "maxlength" attribute, +the Field's max_length attribute will override whatever "maxlength" you specify +in "attrs". +>>> class UserRegistration(Form): +... username = CharField(max_length=10, widget=TextInput(attrs={'maxlength': 20})) +... password = CharField(max_length=10, widget=PasswordInput) +>>> p = UserRegistration() +>>> print p.as_ul() +<li>Username: <input type="text" name="username" maxlength="10" /></li> +<li>Password: <input type="password" name="password" maxlength="10" /></li> + +You can specify the label for a field by using the 'label' argument to a Field +class. If you don't specify 'label', Django will use the field name with +underscores converted to spaces, and the initial letter capitalized. +>>> class UserRegistration(Form): +... username = CharField(max_length=10, label='Your username') +... password1 = CharField(widget=PasswordInput) +... password2 = CharField(widget=PasswordInput, label='Password (again)') +>>> p = UserRegistration() +>>> print p.as_ul() +<li>Your username: <input type="text" name="username" maxlength="10" /></li> +<li>Password1: <input type="password" name="password1" /></li> +<li>Password (again): <input type="password" name="password2" /></li> + # Basic form processing in a view ############################################# >>> from django.template import Template, Context @@ -1862,7 +1949,7 @@ Case 1: GET (an empty form, with no errors). >>> print my_function('GET', {}) <form action="" method="post"> <table> -<tr><td>Username:</td><td><input type="text" name="username" /></td></tr> +<tr><td>Username:</td><td><input type="text" name="username" maxlength="10" /></td></tr> <tr><td>Password1:</td><td><input type="password" name="password1" /></td></tr> <tr><td>Password2:</td><td><input type="password" name="password2" /></td></tr> </table> @@ -1875,7 +1962,7 @@ Case 2: POST with erroneous data (a redisplayed form, with errors). <table> <tr><td colspan="2"><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></td></tr> <tr><td colspan="2"><ul class="errorlist"><li>Ensure this value has at most 10 characters.</li></ul></td></tr> -<tr><td>Username:</td><td><input type="text" name="username" value="this-is-a-long-username" /></td></tr> +<tr><td>Username:</td><td><input type="text" name="username" value="this-is-a-long-username" maxlength="10" /></td></tr> <tr><td>Password1:</td><td><input type="password" name="password1" value="foo" /></td></tr> <tr><td>Password2:</td><td><input type="password" name="password2" value="bar" /></td></tr> </table> @@ -1910,37 +1997,39 @@ particular field. ... </form>''') >>> print t.render(Context({'form': UserRegistration()})) <form action=""> -<p><label>Your username: <input type="text" name="username" /></label></p> +<p><label>Your username: <input type="text" name="username" maxlength="10" /></label></p> <p><label>Password: <input type="password" name="password1" /></label></p> <p><label>Password (again): <input type="password" name="password2" /></label></p> <input type="submit" /> </form> >>> print t.render(Context({'form': UserRegistration({'username': 'django'})})) <form action=""> -<p><label>Your username: <input type="text" name="username" value="django" /></label></p> +<p><label>Your username: <input type="text" name="username" value="django" maxlength="10" /></label></p> <ul class="errorlist"><li>This field is required.</li></ul><p><label>Password: <input type="password" name="password1" /></label></p> <ul class="errorlist"><li>This field is required.</li></ul><p><label>Password (again): <input type="password" name="password2" /></label></p> <input type="submit" /> </form> -Use form.[field].verbose_name to output a field's "verbose name" -- its field -name with underscores converted to spaces, and the initial letter capitalized. +Use form.[field].label to output a field's label. You can specify the label for +a field by using the 'label' argument to a Field class. If you don't specify +'label', Django will use the field name with underscores converted to spaces, +and the initial letter capitalized. >>> t = Template('''<form action=""> -... <p><label>{{ form.username.verbose_name }}: {{ form.username }}</label></p> -... <p><label>{{ form.password1.verbose_name }}: {{ form.password1 }}</label></p> -... <p><label>{{ form.password2.verbose_name }}: {{ form.password2 }}</label></p> +... <p><label>{{ form.username.label }}: {{ form.username }}</label></p> +... <p><label>{{ form.password1.label }}: {{ form.password1 }}</label></p> +... <p><label>{{ form.password2.label }}: {{ form.password2 }}</label></p> ... <input type="submit" /> ... </form>''') >>> print t.render(Context({'form': UserRegistration()})) <form action=""> -<p><label>Username: <input type="text" name="username" /></label></p> +<p><label>Username: <input type="text" name="username" maxlength="10" /></label></p> <p><label>Password1: <input type="password" name="password1" /></label></p> <p><label>Password2: <input type="password" name="password2" /></label></p> <input type="submit" /> </form> -User form.[field].label_tag to output a field's verbose_name with a <label> -tag wrapped around it, but *only* if the given field has an "id" attribute. +User form.[field].label_tag to output a field's label with a <label> tag +wrapped around it, but *only* if the given field has an "id" attribute. Recall from above that passing the "auto_id" argument to a Form gives each field an "id" attribute. >>> t = Template('''<form action=""> @@ -1951,14 +2040,14 @@ field an "id" attribute. ... </form>''') >>> print t.render(Context({'form': UserRegistration()})) <form action=""> -<p>Username: <input type="text" name="username" /></p> +<p>Username: <input type="text" name="username" maxlength="10" /></p> <p>Password1: <input type="password" name="password1" /></p> <p>Password2: <input type="password" name="password2" /></p> <input type="submit" /> </form> >>> print t.render(Context({'form': UserRegistration(auto_id='id_%s')})) <form action=""> -<p><label for="id_username">Username</label>: <input type="text" name="username" id="id_username" /></p> +<p><label for="id_username">Username</label>: <input id="id_username" type="text" name="username" maxlength="10" /></p> <p><label for="id_password1">Password1</label>: <input type="password" name="password1" id="id_password1" /></p> <p><label for="id_password2">Password2</label>: <input type="password" name="password2" id="id_password2" /></p> <input type="submit" /> @@ -1976,7 +2065,7 @@ the list of errors is empty). You can also use it in {% if %} statements. ... </form>''') >>> print t.render(Context({'form': UserRegistration({'username': 'django', 'password1': 'foo', 'password2': 'bar'})})) <form action=""> -<p><label>Your username: <input type="text" name="username" value="django" /></label></p> +<p><label>Your username: <input type="text" name="username" value="django" maxlength="10" /></label></p> <p><label>Password: <input type="password" name="password1" value="foo" /></label></p> <p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p> <input type="submit" /> @@ -1991,7 +2080,7 @@ the list of errors is empty). You can also use it in {% if %} statements. >>> print t.render(Context({'form': UserRegistration({'username': 'django', 'password1': 'foo', 'password2': 'bar'})})) <form action=""> <ul class="errorlist"><li>Please make sure your passwords match.</li></ul> -<p><label>Your username: <input type="text" name="username" value="django" /></label></p> +<p><label>Your username: <input type="text" name="username" value="django" maxlength="10" /></label></p> <p><label>Password: <input type="password" name="password1" value="foo" /></label></p> <p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p> <input type="submit" /> diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 3c31bb0604..0a41f5b5b7 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.conf import settings if __name__ == '__main__': @@ -62,6 +63,11 @@ class OtherClass: def method(self): return "OtherClass.method" +class UnicodeInStrClass: + "Class whose __str__ returns a Unicode object." + def __str__(self): + return u'ŠĐĆŽćžšđ' + class Templates(unittest.TestCase): def test_templates(self): # NOW and NOW_tz are used by timesince tag tests. @@ -173,6 +179,10 @@ class Templates(unittest.TestCase): # Empty strings can be passed as arguments to filters 'basic-syntax36': (r'{{ var|join:"" }}', {'var': ['a', 'b', 'c']}, 'abc'), + # If a variable has a __str__() that returns a Unicode object, the value + # will be converted to a bytestring. + 'basic-syntax37': (r'{{ var }}', {'var': UnicodeInStrClass()}, '\xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91'), + ### COMMENT SYNTAX ######################################################## 'comment-syntax01': ("{# this is hidden #}hello", {}, "hello"), 'comment-syntax02': ("{# this is hidden #}hello{# foo #}", {}, "hello"), @@ -328,18 +338,18 @@ class Templates(unittest.TestCase): 'ifchanged05': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (1, 2, 3)}, '1123123123'), 'ifchanged06': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2)}, '1222'), 'ifchanged07': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% for y in numy %}{% ifchanged %}{{ y }}{% endifchanged %}{% endfor %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2), 'numy': (3, 3, 3)}, '1233323332333'), - + # Test one parameter given to ifchanged. 'ifchanged-param01': ('{% for n in num %}{% ifchanged n %}..{% endifchanged %}{{ n }}{% endfor %}', { 'num': (1,2,3) }, '..1..2..3'), 'ifchanged-param02': ('{% for n in num %}{% for x in numx %}{% ifchanged n %}..{% endifchanged %}{{ x }}{% endfor %}{% endfor %}', { 'num': (1,2,3), 'numx': (5,6,7) }, '..567..567..567'), - + # Test multiple parameters to ifchanged. 'ifchanged-param03': ('{% for n in num %}{{ n }}{% for x in numx %}{% ifchanged x n %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1,1,2), 'numx': (5,6,6) }, '156156256'), - + # Test a date+hour like construct, where the hour of the last day # is the same but the date had changed, so print the hour anyway. 'ifchanged-param04': ('{% for d in days %}{% ifchanged %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'), - + # Logically the same as above, just written with explicit # ifchanged for the day. 'ifchanged-param04': ('{% for d in days %}{% ifchanged d.day %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d.day h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'), |
