From d87c354b65da2b1a3602bbd462f9b694e4519b8d Mon Sep 17 00:00:00 2001 From: Joseph Kocherhans Date: Thu, 7 Dec 2006 23:15:08 +0000 Subject: [PATCH] generic-auth: Merged to trunk [4183]. git-svn-id: http://code.djangoproject.com/svn/django/branches/generic-auth@4184 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../admin/templates/admin/search_form.html | 2 +- django/contrib/admin/views/main.py | 14 +- django/contrib/contenttypes/management.py | 11 +- django/contrib/formtools/__init__.py | 0 django/contrib/formtools/preview.py | 160 ++++++++++++++++++ .../formtools/templates/formtools/form.html | 15 ++ .../templates/formtools/preview.html | 36 ++++ django/contrib/sitemaps/__init__.py | 2 +- django/core/servers/fastcgi.py | 2 + django/newforms/fields.py | 33 ++-- django/newforms/forms.py | 96 +++++------ django/newforms/util.py | 15 +- django/newforms/widgets.py | 10 +- django/template/__init__.py | 6 +- docs/add_ons.txt | 17 ++ docs/newforms.txt | 71 ++++++++ docs/settings.txt | 2 +- docs/testing.txt | 2 +- tests/regressiontests/forms/tests.py | 88 +++++++--- tests/regressiontests/templates/tests.py | 18 +- 20 files changed, 488 insertions(+), 112 deletions(-) create mode 100644 django/contrib/formtools/__init__.py create mode 100644 django/contrib/formtools/preview.py create mode 100644 django/contrib/formtools/templates/formtools/form.html create mode 100644 django/contrib/formtools/templates/formtools/preview.html create mode 100644 docs/newforms.txt 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 @@ {% if show_result_count %} - {% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} ({% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}) + {% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} ({% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}) {% endif %} {% for pair in cl.params.items %} {% ifnotequal pair.0 search_var %}{% endifnotequal %} diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 2312b320ec..d251c95625 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -227,7 +227,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 has_permission(request.user, opts.get_add_permission()): @@ -307,7 +307,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 request.POST and request.POST.has_key("_saveasnew"): @@ -315,8 +315,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 not has_permission(request.user, opts.get_change_permission(), manipulator.original_object): raise PermissionDenied @@ -492,7 +492,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 obj = get_object_or_404(model, pk=object_id) if not has_permission(request.user, opts.get_delete_permission(), obj): @@ -529,7 +529,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. @@ -745,7 +745,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)) # There isn't a specific object to check here, so don't pass one to # has_permission. There should be a has_permission implementation # registered that knows when the obj arg is missing. 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 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\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 %}

Please correct the following errors

{% else %}

Submit

{% endif %} + +
+ +{{ 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 %} + +

Preview your submission

+ + +{% for field in form %} + + + + +{% endfor %} +
{{ field.verbose_name }}:{{ field.data|escape }}
+ +

Security hash: {{ hash_value }}

+ +
+{% for field in form %}{{ field.as_hidden }} +{% endfor %} + + +

+
+ +

Or edit it again

+ +
+ +{{ 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..d676a1c58b 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -2,7 +2,8 @@ Field classes """ -from util import ValidationError, DEFAULT_ENCODING, smart_unicode +from django.utils.translation import gettext +from util import ValidationError, smart_unicode from widgets import TextInput, CheckboxInput, Select, SelectMultiple import datetime import re @@ -50,7 +51,7 @@ 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 class CharField(Field): @@ -64,9 +65,9 @@ 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 class IntegerField(Field): @@ -81,7 +82,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' @@ -113,7 +114,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' @@ -149,7 +150,7 @@ 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): @@ -162,7 +163,7 @@ class RegexField(Field): 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): """ @@ -185,7 +186,7 @@ email_re = re.compile( class EmailField(RegexField): def __init__(self, required=True, widget=None): - RegexField.__init__(self, email_re, u'Enter a valid e-mail address.', required, widget) + RegexField.__init__(self, email_re, gettext(u'Enter a valid e-mail address.'), required, widget) url_re = re.compile( r'^https?://' # http:// or https:// @@ -203,7 +204,7 @@ except ImportError: class URLField(RegexField): def __init__(self, required=True, verify_exists=False, widget=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) self.verify_exists = verify_exists self.user_agent = validator_user_agent @@ -223,9 +224,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): @@ -254,7 +255,7 @@ 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): @@ -266,11 +267,11 @@ class MultipleChoiceField(ChoiceField): 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) @@ -279,7 +280,7 @@ class MultipleChoiceField(ChoiceField): valid_values = set([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): diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 4bc6173249..e0b3d500b5 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 s -- excluding the
." - output = [] - if self.errors.get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append(u'%s' % 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'%s' % 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'%s' % bf.errors) - output.append(u'%s%s' % (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.verbose_name+':')), '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. '') 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 s -- excluding the
." + return self._html_output(u'%(label)s%(field)s', u'%s', '', True) + def as_ul(self): "Returns this form rendered as HTML
  • s -- excluding the
      ." - output = [] - if self.errors.get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append(u'
    • %s
    • ' % 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'
    • %s
    • ' % new_errors) - output.append(str(bf)) - else: - output.append(u'
    • %s%s %s
    • ' % (bf.errors, bf.label_tag(escape(bf.verbose_name+':')), bf)) - return u'\n'.join(output) + return self._html_output(u'
    • %(errors)s%(label)s %(field)s
    • ', u'
    • %s
    • ', '', False) + + def as_p(self): + "Returns this form rendered as HTML

      s." + return self._html_output(u'

      %(label)s %(field)s

      ', u'

      %s

      ', '

      ', True) def non_field_errors(self): """ @@ -155,18 +158,18 @@ 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 - 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 +182,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 +190,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,11 +210,11 @@ 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) + return pretty_name(self.name) verbose_name = property(_verbose_name) def label_tag(self, contents=None): @@ -224,7 +224,7 @@ class BoundField(object): field's HTML-escaped verbose_name. """ contents = contents or escape(self.verbose_name) - widget = self._field.widget + widget = self.field.widget id_ = widget.attrs.get('id') or self.auto_id if id_: contents = '' % (widget.id_for_label(id_), contents) @@ -232,7 +232,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 +240,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'') return u'\n'.join(output) -class RadioInput(object): +class RadioInput(StrAndUnicode): "An object used by RadioFieldRenderer that represents a single ." 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'' % (self.tag(), self.choice_label) def is_checked(self): @@ -168,7 +168,7 @@ class RadioInput(object): final_attrs['checked'] = 'checked' return u'' % 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
        for this set of radio fields." return u'
          \n%s\n
        ' % u'\n'.join([u'
      • %s
      • ' % 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..f796477a9e --- /dev/null +++ b/docs/newforms.txt @@ -0,0 +1,71 @@ +==================== +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. + ```` or ``