diff --git a/AUTHORS b/AUTHORS index f49210d303..645913e7c4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -118,10 +118,12 @@ answer newbie questions, and generally made Django that much better: Manuzhai Petar Marić mark@junklight.com + Yasushi Masuda mattycakes@gmail.com Jason McBrayer mccutchen@gmail.com michael.mcewan@gmail.com + mitakummaa@gmail.com mmarshall Eric Moritz Robin Munn @@ -151,6 +153,7 @@ answer newbie questions, and generally made Django that much better: SmileyChris sopel Thomas Steinacher + nowell strite Radek Švarz Swaroop C H Aaron Swartz @@ -159,6 +162,7 @@ answer newbie questions, and generally made Django that much better: Tom Insam Joe Topjian Karen Tracey + Makoto Tsuyuki Amit Upadhyay Geert Vanderkelen Milton Waddams diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 36fee9ec6d..245096590d 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -25,7 +25,7 @@ ADMINS = () INTERNAL_IPS = () # Local time zone for this installation. All choices can be found here: -# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index d6f34a28db..a44bc172f0 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -17,7 +17,7 @@ DATABASE_HOST = '' # Set to empty string for localhost. Not used wit DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. # Local time zone for this installation. All choices can be found here: -# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: diff --git a/django/conf/project_template/urls.py b/django/conf/project_template/urls.py index 4483014173..402dd6536b 100644 --- a/django/conf/project_template/urls.py +++ b/django/conf/project_template/urls.py @@ -2,7 +2,7 @@ from django.conf.urls.defaults import * urlpatterns = patterns('', # Example: - # (r'^{{ project_name }}/', include('{{ project_name }}.apps.foo.urls.foo')), + # (r'^{{ project_name }}/', include('{{ project_name }}.foo.urls')), # Uncomment this for admin: # (r'^admin/', include('django.contrib.admin.urls')), diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index b63604b268..d3e8c96b91 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -38,7 +38,10 @@
{% block pretitle %}{% endblock %} {% block content_title %}{% if title %}

{{ title|escape }}

{% endif %}{% endblock %} - {% block content %}{{ content }}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} {% block sidebar %}{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 02d05f9ad5..71a4df03e1 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -16,12 +16,14 @@ {% endif %}{% endblock %} {% block content %}
+{% block object-tools %} {% if change %}{% if not is_popup %} {% endif %}{% endif %} +{% endblock %}
{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index bd2304bd52..f50a73c934 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -7,9 +7,11 @@ {% block coltype %}flex{% endblock %} {% block content %}
+{% block object-tools %} {% if has_add_permission %} {% endif %} +{% endblock %}
{% block search %}{% search_form cl %}{% endblock %} {% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %} 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/auth.py b/django/contrib/admin/views/auth.py index 06e469e5d2..52bf3bcde8 100644 --- a/django/contrib/admin/views/auth.py +++ b/django/contrib/admin/views/auth.py @@ -2,7 +2,7 @@ from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied -from django import forms, template +from django import oldforms, template from django.shortcuts import render_to_response from django.http import HttpResponseRedirect @@ -24,7 +24,7 @@ def user_add_stage(request): return HttpResponseRedirect('../%s/' % new_user.id) else: errors = new_data = {} - form = forms.FormWrapper(manipulator, new_data, errors) + form = oldforms.FormWrapper(manipulator, new_data, errors) return render_to_response('admin/auth/user/add_form.html', { 'title': _('Add user'), 'form': form, diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index e0cfb169e0..aa8b35bb96 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,4 +1,4 @@ -from django import forms, template +from django import oldforms, template from django.conf import settings from django.contrib.admin.filterspecs import FilterSpec from django.contrib.admin.views.decorators import staff_member_required @@ -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 request.user.has_perm(app_label + '.' + opts.get_add_permission()): @@ -299,7 +299,7 @@ def add_stage(request, app_label, model_name, show_delete=False, form_url='', po errors = {} # Populate the FormWrapper. - form = forms.FormWrapper(manipulator, new_data, errors) + form = oldforms.FormWrapper(manipulator, new_data, errors) c = template.RequestContext(request, { 'title': _('Add %s') % opts.verbose_name, @@ -318,13 +318,13 @@ 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 try: manipulator = model.ChangeManipulator(object_id) except ObjectDoesNotExist: - raise Http404 + raise Http404('%s object with primary key %r does not exist' % (model_name, escape(object_id))) if not request.user.has_perm(app_label + '.' + opts.get_change_permission(), object=manipulator.original_object): raise PermissionDenied @@ -332,8 +332,6 @@ def change_stage(request, app_label, model_name, object_id): if request.POST and request.POST.has_key("_saveasnew"): return add_stage(request, app_label, model_name, form_url='../../add/') - - if request.POST: new_data = request.POST.copy() @@ -400,7 +398,7 @@ def change_stage(request, app_label, model_name, object_id): errors = {} # Populate the FormWrapper. - form = forms.FormWrapper(manipulator, new_data, errors) + form = oldforms.FormWrapper(manipulator, new_data, errors) form.original = manipulator.original_object form.order_objects = [] @@ -519,7 +517,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) @@ -558,7 +556,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. @@ -788,7 +786,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.contains_permission(app_label + '.' + model._meta.get_change_permission(), model): raise PermissionDenied try: diff --git a/django/contrib/admin/views/template.py b/django/contrib/admin/views/template.py index 93d110b045..a3b4538b10 100644 --- a/django/contrib/admin/views/template.py +++ b/django/contrib/admin/views/template.py @@ -1,6 +1,6 @@ from django.contrib.admin.views.decorators import staff_member_required from django.core import validators -from django import template, forms +from django import template, oldforms from django.template import loader from django.shortcuts import render_to_response from django.contrib.sites.models import Site @@ -25,17 +25,17 @@ def template_validator(request): request.user.message_set.create(message='The template is valid.') return render_to_response('admin/template_validator.html', { 'title': 'Template validator', - 'form': forms.FormWrapper(manipulator, new_data, errors), + 'form': oldforms.FormWrapper(manipulator, new_data, errors), }, context_instance=template.RequestContext(request)) template_validator = staff_member_required(template_validator) -class TemplateValidator(forms.Manipulator): +class TemplateValidator(oldforms.Manipulator): def __init__(self, settings_modules): self.settings_modules = settings_modules site_list = Site.objects.in_bulk(settings_modules.keys()).values() self.fields = ( - forms.SelectField('site', is_required=True, choices=[(s.id, s.name) for s in site_list]), - forms.LargeTextField('template', is_required=True, rows=25, validator_list=[self.isValidTemplate]), + oldforms.SelectField('site', is_required=True, choices=[(s.id, s.name) for s in site_list]), + oldforms.LargeTextField('template', is_required=True, rows=25, validator_list=[self.isValidTemplate]), ) def isValidTemplate(self, field_data, all_data): diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 24c69cb73e..aea52d1f2a 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -3,16 +3,16 @@ from django.contrib.auth import authenticate from django.contrib.sites.models import Site from django.template import Context, loader from django.core import validators -from django import forms +from django import oldforms -class UserCreationForm(forms.Manipulator): +class UserCreationForm(oldforms.Manipulator): "A form that creates a user, with no privileges, from the given username and password." def __init__(self): self.fields = ( - forms.TextField(field_name='username', length=30, maxlength=30, is_required=True, + oldforms.TextField(field_name='username', length=30, maxlength=30, is_required=True, validator_list=[validators.isAlphaNumeric, self.isValidUsername]), - forms.PasswordField(field_name='password1', length=30, maxlength=60, is_required=True), - forms.PasswordField(field_name='password2', length=30, maxlength=60, is_required=True, + oldforms.PasswordField(field_name='password1', length=30, maxlength=60, is_required=True), + oldforms.PasswordField(field_name='password2', length=30, maxlength=60, is_required=True, validator_list=[validators.AlwaysMatchesOtherField('password1', _("The two password fields didn't match."))]), ) @@ -27,7 +27,7 @@ class UserCreationForm(forms.Manipulator): "Creates the user." return User.objects.create_user(new_data['username'], '', new_data['password1']) -class AuthenticationForm(forms.Manipulator): +class AuthenticationForm(oldforms.Manipulator): """ Base class for authenticating users. Extend this to get a form that accepts username/password logins. @@ -41,9 +41,9 @@ class AuthenticationForm(forms.Manipulator): """ self.request = request self.fields = [ - forms.TextField(field_name="username", length=15, maxlength=30, is_required=True, + oldforms.TextField(field_name="username", length=15, maxlength=30, is_required=True, validator_list=[self.isValidUser, self.hasCookiesEnabled]), - forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True), + oldforms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True), ] self.user_cache = None @@ -68,11 +68,11 @@ class AuthenticationForm(forms.Manipulator): def get_user(self): return self.user_cache -class PasswordResetForm(forms.Manipulator): +class PasswordResetForm(oldforms.Manipulator): "A form that lets a user request a password reset" def __init__(self): self.fields = ( - forms.EmailField(field_name="email", length=40, is_required=True, + oldforms.EmailField(field_name="email", length=40, is_required=True, validator_list=[self.isValidUserEmail]), ) @@ -105,16 +105,16 @@ class PasswordResetForm(forms.Manipulator): } send_mail('Password reset on %s' % site_name, t.render(Context(c)), None, [self.user_cache.email]) -class PasswordChangeForm(forms.Manipulator): +class PasswordChangeForm(oldforms.Manipulator): "A form that lets a user change his password." def __init__(self, user): self.user = user self.fields = ( - forms.PasswordField(field_name="old_password", length=30, maxlength=30, is_required=True, + oldforms.PasswordField(field_name="old_password", length=30, maxlength=30, is_required=True, validator_list=[self.isValidOldPassword]), - forms.PasswordField(field_name="new_password1", length=30, maxlength=30, is_required=True, + oldforms.PasswordField(field_name="new_password1", length=30, maxlength=30, is_required=True, validator_list=[validators.AlwaysMatchesOtherField('new_password2', _("The two 'new password' fields didn't match."))]), - forms.PasswordField(field_name="new_password2", length=30, maxlength=30, is_required=True), + oldforms.PasswordField(field_name="new_password2", length=30, maxlength=30, is_required=True), ) def isValidOldPassword(self, new_data, all_data): diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 6882755787..fda17b91fb 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -1,6 +1,6 @@ from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm -from django import forms +from django import oldforms from django.shortcuts import render_to_response from django.template import RequestContext from django.contrib.sites.models import Site @@ -26,7 +26,7 @@ def login(request, template_name='registration/login.html'): errors = {} request.session.set_test_cookie() return render_to_response(template_name, { - 'form': forms.FormWrapper(manipulator, request.POST, errors), + 'form': oldforms.FormWrapper(manipulator, request.POST, errors), REDIRECT_FIELD_NAME: redirect_to, 'site_name': Site.objects.get_current().name, }, context_instance=RequestContext(request)) @@ -62,7 +62,7 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas else: form.save(email_template_name=email_template_name) return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response(template_name, {'form': forms.FormWrapper(form, new_data, errors)}, + return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)}, context_instance=RequestContext(request)) def password_reset_done(request, template_name='registration/password_reset_done.html'): @@ -77,7 +77,7 @@ def password_change(request, template_name='registration/password_change_form.ht if not errors: form.save(new_data) return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response(template_name, {'form': forms.FormWrapper(form, new_data, errors)}, + return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)}, context_instance=RequestContext(request)) password_change = login_required(password_change) diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 3640da90fe..12330afe41 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -1,5 +1,5 @@ from django.core import validators -from django import forms +from django import oldforms from django.core.mail import mail_admins, mail_managers from django.http import Http404 from django.core.exceptions import ObjectDoesNotExist @@ -28,37 +28,37 @@ class PublicCommentManipulator(AuthenticationForm): else: return [] self.fields.extend([ - forms.LargeTextField(field_name="comment", maxlength=3000, is_required=True, + oldforms.LargeTextField(field_name="comment", maxlength=3000, is_required=True, validator_list=[self.hasNoProfanities]), - forms.RadioSelectField(field_name="rating1", choices=choices, + oldforms.RadioSelectField(field_name="rating1", choices=choices, is_required=ratings_required and num_rating_choices > 0, validator_list=get_validator_list(1), ), - forms.RadioSelectField(field_name="rating2", choices=choices, + oldforms.RadioSelectField(field_name="rating2", choices=choices, is_required=ratings_required and num_rating_choices > 1, validator_list=get_validator_list(2), ), - forms.RadioSelectField(field_name="rating3", choices=choices, + oldforms.RadioSelectField(field_name="rating3", choices=choices, is_required=ratings_required and num_rating_choices > 2, validator_list=get_validator_list(3), ), - forms.RadioSelectField(field_name="rating4", choices=choices, + oldforms.RadioSelectField(field_name="rating4", choices=choices, is_required=ratings_required and num_rating_choices > 3, validator_list=get_validator_list(4), ), - forms.RadioSelectField(field_name="rating5", choices=choices, + oldforms.RadioSelectField(field_name="rating5", choices=choices, is_required=ratings_required and num_rating_choices > 4, validator_list=get_validator_list(5), ), - forms.RadioSelectField(field_name="rating6", choices=choices, + oldforms.RadioSelectField(field_name="rating6", choices=choices, is_required=ratings_required and num_rating_choices > 5, validator_list=get_validator_list(6), ), - forms.RadioSelectField(field_name="rating7", choices=choices, + oldforms.RadioSelectField(field_name="rating7", choices=choices, is_required=ratings_required and num_rating_choices > 6, validator_list=get_validator_list(7), ), - forms.RadioSelectField(field_name="rating8", choices=choices, + oldforms.RadioSelectField(field_name="rating8", choices=choices, is_required=ratings_required and num_rating_choices > 7, validator_list=get_validator_list(8), ), @@ -117,13 +117,13 @@ class PublicCommentManipulator(AuthenticationForm): mail_managers("Comment posted by sketchy user (%s)" % self.user_cache.username, c.get_as_text()) return c -class PublicFreeCommentManipulator(forms.Manipulator): +class PublicFreeCommentManipulator(oldforms.Manipulator): "Manipulator that handles public free (unregistered) comments" def __init__(self): self.fields = ( - forms.TextField(field_name="person_name", maxlength=50, is_required=True, + oldforms.TextField(field_name="person_name", maxlength=50, is_required=True, validator_list=[self.hasNoProfanities]), - forms.LargeTextField(field_name="comment", maxlength=3000, is_required=True, + oldforms.LargeTextField(field_name="comment", maxlength=3000, is_required=True, validator_list=[self.hasNoProfanities]), ) @@ -221,9 +221,9 @@ def post_comment(request): from django.contrib.auth import login login(request, manipulator.get_user()) if errors or request.POST.has_key('preview'): - class CommentFormWrapper(forms.FormWrapper): + class CommentFormWrapper(oldforms.FormWrapper): def __init__(self, manipulator, new_data, errors, rating_choices): - forms.FormWrapper.__init__(self, manipulator, new_data, errors) + oldforms.FormWrapper.__init__(self, manipulator, new_data, errors) self.rating_choices = rating_choices def ratings(self): field_list = [self['rating%d' % (i+1)] for i in range(len(rating_choices))] @@ -302,7 +302,7 @@ def post_free_comment(request): comment = errors and '' or manipulator.get_comment(new_data) return render_to_response('comments/free_preview.html', { 'comment': comment, - 'comment_form': forms.FormWrapper(manipulator, new_data, errors), + 'comment_form': oldforms.FormWrapper(manipulator, new_data, errors), 'options': options, 'target': target, 'hash': security_hash, 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/csrf/middleware.py b/django/contrib/csrf/middleware.py index f6f78867dc..93a9484ca6 100644 --- a/django/contrib/csrf/middleware.py +++ b/django/contrib/csrf/middleware.py @@ -11,7 +11,7 @@ import md5 import re import itertools -_ERROR_MSG = "

403 Forbidden

Cross Site Request Forgery detected. Request aborted.

" +_ERROR_MSG = '

403 Forbidden

Cross Site Request Forgery detected. Request aborted.

' _POST_FORM_RE = \ re.compile(r'(]*\bmethod=(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) 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/handlers/base.py b/django/core/handlers/base.py index c1403ea4fa..ca48b301d4 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -60,7 +60,10 @@ class BaseHandler(object): if response: return response - resolver = urlresolvers.RegexURLResolver(r'^/', settings.ROOT_URLCONF) + # Get urlconf from request object, if available. Otherwise use default. + urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF) + + resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) try: callback, callback_args, callback_kwargs = resolver.resolve(request.path) @@ -84,7 +87,11 @@ class BaseHandler(object): # Complain if the view returned None (a common error). if response is None: - raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, callback.func_name) + try: + view_name = callback.func_name # If it's a function + except AttributeError: + view_name = callback.__class__.__name__ + '.__call__' # If it's a class + raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name) return response except http.Http404, e: diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 7dc1a4a5eb..71cfecd9a0 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -62,7 +62,7 @@ def safe_copyfileobj(fsrc, fdst, length=16*1024, size=0): data in the body. """ if not size: - return copyfileobj(fsrc, fdst, length) + return while size > 0: buf = fsrc.read(min(length, size)) if not buf: @@ -157,8 +157,11 @@ class WSGIRequest(http.HttpRequest): return self._raw_post_data except AttributeError: buf = StringIO() - # CONTENT_LENGTH might be absent if POST doesn't have content at all (lighttpd) - content_length = int(self.environ.get('CONTENT_LENGTH', 0)) + try: + # CONTENT_LENGTH might be absent if POST doesn't have content at all (lighttpd) + content_length = int(self.environ.get('CONTENT_LENGTH', 0)) + except ValueError: # if CONTENT_LENGTH was empty string or not an integer + content_length = 0 safe_copyfileobj(self.environ['wsgi.input'], buf, size=content_length) self._raw_post_data = buf.getvalue() buf.close() 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/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 1e48e9c3fa..e44bc0b560 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -118,7 +118,7 @@ def get_pk_default_value(): try: Database.register_type(Database.new_type((1082,), "DATE", util.typecast_date)) except AttributeError: - raise Exception, "You appear to be using psycopg version 2, which isn't supported yet, because it's still in beta. Use psycopg version 1 instead: http://initd.org/projects/psycopg1" + raise Exception, "You appear to be using psycopg version 2. Set your DATABASE_ENGINE to 'postgresql_psycopg2' instead of 'postgresql'." Database.register_type(Database.new_type((1083,1266), "TIME", util.typecast_time)) Database.register_type(Database.new_type((1114,1184), "TIMESTAMP", util.typecast_timestamp)) Database.register_type(Database.new_type((16,), "BOOLEAN", util.typecast_boolean)) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index bc1eaf0a6a..fe317ac24f 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -2,7 +2,8 @@ from django.db.models import signals from django.dispatch import dispatcher from django.conf import settings from django.core import validators -from django import forms +from django import oldforms +from django import newforms as forms from django.core.exceptions import ObjectDoesNotExist from django.utils.functional import curry from django.utils.itercompat import tee @@ -206,10 +207,10 @@ class Field(object): if self.choices: if self.radio_admin: - field_objs = [forms.RadioSelectField] + field_objs = [oldforms.RadioSelectField] params['ul_class'] = get_ul_class(self.radio_admin) else: - field_objs = [forms.SelectField] + field_objs = [oldforms.SelectField] params['choices'] = self.get_choices_default() else: @@ -218,7 +219,7 @@ class Field(object): def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): """ - Returns a list of forms.FormField instances for this field. It + Returns a list of oldforms.FormField instances for this field. It calculates the choices at runtime, not at compile time. name_prefix is a prefix to prepend to the "field_name" argument. @@ -333,6 +334,11 @@ class Field(object): return self._choices choices = property(_get_choices) + def formfield(self): + "Returns a django.newforms.Field instance for this database Field." + # TODO: This is just a temporary default during development. + return forms.CharField(required=not self.blank, label=capfirst(self.verbose_name)) + class AutoField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): @@ -354,7 +360,7 @@ class AutoField(Field): return Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow) def get_manipulator_field_objs(self): - return [forms.HiddenField] + return [oldforms.HiddenField] def get_manipulator_new_data(self, new_data, rel=False): # Never going to be called @@ -369,6 +375,9 @@ class AutoField(Field): super(AutoField, self).contribute_to_class(cls, name) cls._meta.has_auto_field = True + def formfield(self): + return None + class BooleanField(Field): def __init__(self, *args, **kwargs): kwargs['blank'] = True @@ -381,11 +390,14 @@ class BooleanField(Field): raise validators.ValidationError, gettext("This value must be either True or False.") def get_manipulator_field_objs(self): - return [forms.CheckboxField] + return [oldforms.CheckboxField] + + def formfield(self): + return forms.BooleanField(required=not self.blank, label=capfirst(self.verbose_name)) class CharField(Field): def get_manipulator_field_objs(self): - return [forms.TextField] + return [oldforms.TextField] def to_python(self, value): if isinstance(value, basestring): @@ -397,10 +409,13 @@ class CharField(Field): raise validators.ValidationError, gettext_lazy("This field cannot be null.") return str(value) + def formfield(self): + return forms.CharField(max_length=self.maxlength, required=not self.blank, label=capfirst(self.verbose_name)) + # TODO: Maybe move this into contrib, because it's specialized. class CommaSeparatedIntegerField(CharField): def get_manipulator_field_objs(self): - return [forms.CommaSeparatedIntegerField] + return [oldforms.CommaSeparatedIntegerField] class DateField(Field): empty_strings_allowed = False @@ -457,19 +472,20 @@ class DateField(Field): def get_db_prep_save(self, value): # Casts dates into string format for entry into database. - if isinstance(value, datetime.datetime): - value = value.date().strftime('%Y-%m-%d') - elif isinstance(value, datetime.date): + if value is not None: value = value.strftime('%Y-%m-%d') return Field.get_db_prep_save(self, value) def get_manipulator_field_objs(self): - return [forms.DateField] + return [oldforms.DateField] def flatten_data(self, follow, obj = None): val = self._get_val_from_obj(obj) return {self.attname: (val is not None and val.strftime("%Y-%m-%d") or '')} + def formfield(self): + return forms.DateField(required=not self.blank, label=capfirst(self.verbose_name)) + class DateTimeField(DateField): def to_python(self, value): if isinstance(value, datetime.datetime): @@ -489,19 +505,12 @@ class DateTimeField(DateField): def get_db_prep_save(self, value): # Casts dates into string format for entry into database. - if isinstance(value, datetime.datetime): + if value is not None: # MySQL will throw a warning if microseconds are given, because it # doesn't support microseconds. if settings.DATABASE_ENGINE == 'mysql' and hasattr(value, 'microsecond'): value = value.replace(microsecond=0) value = str(value) - elif isinstance(value, datetime.date): - # MySQL will throw a warning if microseconds are given, because it - # doesn't support microseconds. - if settings.DATABASE_ENGINE == 'mysql' and hasattr(value, 'microsecond'): - value = datetime.datetime(value.year, value.month, value.day, microsecond=0) - value = str(value) - return Field.get_db_prep_save(self, value) def get_db_prep_lookup(self, lookup_type, value): @@ -512,7 +521,7 @@ class DateTimeField(DateField): return Field.get_db_prep_lookup(self, lookup_type, value) def get_manipulator_field_objs(self): - return [forms.DateField, forms.TimeField] + return [oldforms.DateField, oldforms.TimeField] def get_manipulator_field_names(self, name_prefix): return [name_prefix + self.name + '_date', name_prefix + self.name + '_time'] @@ -535,6 +544,9 @@ class DateTimeField(DateField): return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), time_field: (val is not None and val.strftime("%H:%M:%S") or '')} + def formfield(self): + return forms.DateTimeField(required=not self.blank, label=capfirst(self.verbose_name)) + class EmailField(CharField): def __init__(self, *args, **kwargs): kwargs['maxlength'] = 75 @@ -544,11 +556,14 @@ class EmailField(CharField): return "CharField" def get_manipulator_field_objs(self): - return [forms.EmailField] + return [oldforms.EmailField] def validate(self, field_data, all_data): validators.isValidEmail(field_data, all_data) + def formfield(self): + return forms.EmailField(required=not self.blank, label=capfirst(self.verbose_name)) + class FileField(Field): def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs): self.upload_to = upload_to @@ -608,7 +623,7 @@ class FileField(Field): os.remove(file_name) def get_manipulator_field_objs(self): - return [forms.FileUploadField, forms.HiddenField] + return [oldforms.FileUploadField, oldforms.HiddenField] def get_manipulator_field_names(self, name_prefix): return [name_prefix + self.name + '_file', name_prefix + self.name] @@ -636,7 +651,7 @@ class FilePathField(Field): Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [curry(forms.FilePathField, path=self.path, match=self.match, recursive=self.recursive)] + return [curry(oldforms.FilePathField, path=self.path, match=self.match, recursive=self.recursive)] class FloatField(Field): empty_strings_allowed = False @@ -645,7 +660,7 @@ class FloatField(Field): Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [curry(forms.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)] + return [curry(oldforms.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)] class ImageField(FileField): def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs): @@ -653,7 +668,7 @@ class ImageField(FileField): FileField.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [forms.ImageUploadField, forms.HiddenField] + return [oldforms.ImageUploadField, oldforms.HiddenField] def contribute_to_class(self, cls, name): super(ImageField, self).contribute_to_class(cls, name) @@ -679,7 +694,10 @@ class ImageField(FileField): class IntegerField(Field): empty_strings_allowed = False def get_manipulator_field_objs(self): - return [forms.IntegerField] + return [oldforms.IntegerField] + + def formfield(self): + return forms.IntegerField(required=not self.blank, label=capfirst(self.verbose_name)) class IPAddressField(Field): def __init__(self, *args, **kwargs): @@ -687,7 +705,7 @@ class IPAddressField(Field): Field.__init__(self, *args, **kwargs) def get_manipulator_field_objs(self): - return [forms.IPAddressField] + return [oldforms.IPAddressField] def validate(self, field_data, all_data): validators.isValidIPAddress4(field_data, None) @@ -698,22 +716,22 @@ class NullBooleanField(Field): Field.__init__(self, *args, **kwargs) def get_manipulator_field_objs(self): - return [forms.NullBooleanField] + return [oldforms.NullBooleanField] class PhoneNumberField(IntegerField): def get_manipulator_field_objs(self): - return [forms.PhoneNumberField] + return [oldforms.PhoneNumberField] def validate(self, field_data, all_data): validators.isValidPhone(field_data, all_data) class PositiveIntegerField(IntegerField): def get_manipulator_field_objs(self): - return [forms.PositiveIntegerField] + return [oldforms.PositiveIntegerField] class PositiveSmallIntegerField(IntegerField): def get_manipulator_field_objs(self): - return [forms.PositiveSmallIntegerField] + return [oldforms.PositiveSmallIntegerField] class SlugField(Field): def __init__(self, *args, **kwargs): @@ -725,15 +743,15 @@ class SlugField(Field): Field.__init__(self, *args, **kwargs) def get_manipulator_field_objs(self): - return [forms.TextField] + return [oldforms.TextField] class SmallIntegerField(IntegerField): def get_manipulator_field_objs(self): - return [forms.SmallIntegerField] + return [oldforms.SmallIntegerField] class TextField(Field): def get_manipulator_field_objs(self): - return [forms.LargeTextField] + return [oldforms.LargeTextField] class TimeField(Field): empty_strings_allowed = False @@ -769,24 +787,31 @@ class TimeField(Field): return Field.get_db_prep_save(self, value) def get_manipulator_field_objs(self): - return [forms.TimeField] + return [oldforms.TimeField] def flatten_data(self,follow, obj = None): val = self._get_val_from_obj(obj) return {self.attname: (val is not None and val.strftime("%H:%M:%S") or '')} + def formfield(self): + return forms.TimeField(required=not self.blank, label=capfirst(self.verbose_name)) + class URLField(Field): def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): if verify_exists: kwargs.setdefault('validator_list', []).append(validators.isExistingURL) + self.verify_exists = verify_exists Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [forms.URLField] + return [oldforms.URLField] + + def formfield(self): + return forms.URLField(required=not self.blank, verify_exists=self.verify_exists, label=capfirst(self.verbose_name)) class USStateField(Field): def get_manipulator_field_objs(self): - return [forms.USStateField] + return [oldforms.USStateField] class XMLField(TextField): def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs): @@ -797,7 +822,7 @@ class XMLField(TextField): return "TextField" def get_manipulator_field_objs(self): - return [curry(forms.XMLLargeTextField, schema_path=self.schema_path)] + return [curry(oldforms.XMLLargeTextField, schema_path=self.schema_path)] class OrderingField(IntegerField): empty_strings_allowed=False @@ -810,4 +835,4 @@ class OrderingField(IntegerField): return "IntegerField" def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): - return [forms.HiddenField(name_prefix + self.name)] + return [oldforms.HiddenField(name_prefix + self.name)] diff --git a/django/db/models/fields/generic.py b/django/db/models/fields/generic.py index 7d7651029c..1ad8346e42 100644 --- a/django/db/models/fields/generic.py +++ b/django/db/models/fields/generic.py @@ -2,7 +2,7 @@ Classes allowing "generic" relations through ContentType and object-id fields. """ -from django import forms +from django import oldforms from django.core.exceptions import ObjectDoesNotExist from django.db import backend from django.db.models import signals @@ -98,7 +98,7 @@ class GenericRelation(RelatedField, Field): def get_manipulator_field_objs(self): choices = self.get_choices_default() - return [curry(forms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] + return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] def get_choices_default(self): return Field.get_choices(self, include_blank=False) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index bd9262d55a..797ef05be1 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -5,7 +5,7 @@ from django.db.models.related import RelatedObject from django.utils.translation import gettext_lazy, string_concat, ngettext from django.utils.functional import curry from django.core import validators -from django import forms +from django import oldforms from django.dispatch import dispatcher # For Python 2.3 @@ -256,8 +256,7 @@ class ForeignRelatedObjectsDescriptor(object): # Otherwise, just move the named objects into the set. if self.related.field.null: manager.clear() - for obj in value: - manager.add(obj) + manager.add(*value) def create_many_related_manager(superclass): """Creates a manager that subclasses 'superclass' (which is a Manager) @@ -318,25 +317,31 @@ def create_many_related_manager(superclass): # *objs - objects to add from django.db import connection - # Add the newly created or already existing objects to the join table. - # First find out which items are already added, to avoid adding them twice - new_ids = set([obj._get_pk_val() for obj in objs]) - cursor = connection.cursor() - cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (target_col_name, self.join_table, source_col_name, - target_col_name, ",".join(['%s'] * len(new_ids))), - [self._pk_val] + list(new_ids)) - if cursor.rowcount is not None and cursor.rowcount != 0: - existing_ids = set([row[0] for row in cursor.fetchmany(cursor.rowcount)]) - else: - existing_ids = set() + # If there aren't any objects, there is nothing to do. + if objs: + # Check that all the objects are of the right type + for obj in objs: + if not isinstance(obj, self.model): + raise ValueError, "objects to add() must be %s instances" % self.model._meta.object_name + # Add the newly created or already existing objects to the join table. + # First find out which items are already added, to avoid adding them twice + new_ids = set([obj._get_pk_val() for obj in objs]) + cursor = connection.cursor() + cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ + (target_col_name, self.join_table, source_col_name, + target_col_name, ",".join(['%s'] * len(new_ids))), + [self._pk_val] + list(new_ids)) + if cursor.rowcount is not None and cursor.rowcount != 0: + existing_ids = set([row[0] for row in cursor.fetchmany(cursor.rowcount)]) + else: + existing_ids = set() - # Add the ones that aren't there already - for obj_id in (new_ids - existing_ids): - cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (self.join_table, source_col_name, target_col_name), - [self._pk_val, obj_id]) - transaction.commit_unless_managed() + # Add the ones that aren't there already + for obj_id in (new_ids - existing_ids): + cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ + (self.join_table, source_col_name, target_col_name), + [self._pk_val, obj_id]) + transaction.commit_unless_managed() def _remove_items(self, source_col_name, target_col_name, *objs): # source_col_name: the PK colname in join_table for the source object @@ -344,16 +349,20 @@ def create_many_related_manager(superclass): # *objs - objects to remove from django.db import connection - for obj in objs: - if not isinstance(obj, self.model): - raise ValueError, "objects to remove() must be %s instances" % self.model._meta.object_name - # Remove the specified objects from the join table - cursor = connection.cursor() - for obj in objs: - cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s = %%s" % \ - (self.join_table, source_col_name, target_col_name), - [self._pk_val, obj._get_pk_val()]) - transaction.commit_unless_managed() + # If there aren't any objects, there is nothing to do. + if objs: + # Check that all the objects are of the right type + for obj in objs: + if not isinstance(obj, self.model): + raise ValueError, "objects to remove() must be %s instances" % self.model._meta.object_name + # Remove the specified objects from the join table + old_ids = set([obj._get_pk_val() for obj in objs]) + cursor = connection.cursor() + cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ + (self.join_table, source_col_name, + target_col_name, ",".join(['%s'] * len(old_ids))), + [self._pk_val] + list(old_ids)) + transaction.commit_unless_managed() def _clear_items(self, source_col_name): # source_col_name: the PK colname in join_table for the source object @@ -405,8 +414,7 @@ class ManyRelatedObjectsDescriptor(object): manager = self.__get__(instance) manager.clear() - for obj in value: - manager.add(obj) + manager.add(*value) class ReverseManyRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object @@ -447,8 +455,7 @@ class ReverseManyRelatedObjectsDescriptor(object): manager = self.__get__(instance) manager.clear() - for obj in value: - manager.add(obj) + manager.add(*value) class ForeignKey(RelatedField, Field): empty_strings_allowed = False @@ -493,13 +500,13 @@ class ForeignKey(RelatedField, Field): params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) else: if self.radio_admin: - field_objs = [forms.RadioSelectField] + field_objs = [oldforms.RadioSelectField] params['ul_class'] = get_ul_class(self.radio_admin) else: if self.null: - field_objs = [forms.NullSelectField] + field_objs = [oldforms.NullSelectField] else: - field_objs = [forms.SelectField] + field_objs = [oldforms.SelectField] params['choices'] = self.get_choices_default() return field_objs, params @@ -508,7 +515,7 @@ class ForeignKey(RelatedField, Field): if self.rel.raw_id_admin and not isinstance(rel_field, AutoField): return rel_field.get_manipulator_field_objs() else: - return [forms.IntegerField] + return [oldforms.IntegerField] def get_db_prep_save(self, value): if value == '' or value == None: @@ -581,13 +588,13 @@ class OneToOneField(RelatedField, IntegerField): params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) else: if self.radio_admin: - field_objs = [forms.RadioSelectField] + field_objs = [oldforms.RadioSelectField] params['ul_class'] = get_ul_class(self.radio_admin) else: if self.null: - field_objs = [forms.NullSelectField] + field_objs = [oldforms.NullSelectField] else: - field_objs = [forms.SelectField] + field_objs = [oldforms.SelectField] params['choices'] = self.get_choices_default() return field_objs, params @@ -622,10 +629,10 @@ class ManyToManyField(RelatedField, Field): def get_manipulator_field_objs(self): if self.rel.raw_id_admin: - return [forms.RawIdAdminField] + return [oldforms.RawIdAdminField] else: choices = self.get_choices_default() - return [curry(forms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] + return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] def get_choices_default(self): return Field.get_choices(self, include_blank=False) diff --git a/django/db/models/manipulators.py b/django/db/models/manipulators.py index 866e12781c..e9dfa7037c 100644 --- a/django/db/models/manipulators.py +++ b/django/db/models/manipulators.py @@ -1,5 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist -from django import forms +from django import oldforms from django.core import validators from django.db.models.fields import FileField, AutoField from django.dispatch import dispatcher @@ -40,7 +40,7 @@ class ManipulatorDescriptor(object): self.man._prepare(model) return self.man -class AutomaticManipulator(forms.Manipulator): +class AutomaticManipulator(oldforms.Manipulator): def _prepare(cls, model): cls.model = model cls.manager = model._default_manager @@ -76,7 +76,7 @@ class AutomaticManipulator(forms.Manipulator): # Add field for ordering. if self.change and self.opts.get_ordered_objects(): - self.fields.append(forms.CommaSeparatedIntegerField(field_name="order_")) + self.fields.append(oldforms.CommaSeparatedIntegerField(field_name="order_")) def save(self, new_data): # TODO: big cleanup when core fields go -> use recursive manipulators. @@ -313,7 +313,7 @@ def manipulator_validator_unique_together(field_name_list, opts, self, field_dat def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_type, self, field_data, all_data): from django.db.models.fields.related import ManyToOneRel date_str = all_data.get(date_field.get_manipulator_field_names('')[0], None) - date_val = forms.DateField.html2python(date_str) + date_val = oldforms.DateField.html2python(date_str) if date_val is None: return # Date was invalid. This will be caught by another validator. lookup_kwargs = {'%s__year' % date_field.name: date_val.year} diff --git a/django/forms/__init__.py b/django/forms/__init__.py index 0b9ac05edb..68d3d245a2 100644 --- a/django/forms/__init__.py +++ b/django/forms/__init__.py @@ -1,1008 +1 @@ -from django.core import validators -from django.core.exceptions import PermissionDenied -from django.utils.html import escape -from django.conf import settings -from django.utils.translation import gettext, ngettext - -FORM_FIELD_ID_PREFIX = 'id_' - -class EmptyValue(Exception): - "This is raised when empty data is provided" - pass - -class Manipulator(object): - # List of permission strings. User must have at least one to manipulate. - # None means everybody has permission. - required_permission = '' - - def __init__(self): - # List of FormField objects - self.fields = [] - - def __getitem__(self, field_name): - "Looks up field by field name; raises KeyError on failure" - for field in self.fields: - if field.field_name == field_name: - return field - raise KeyError, "Field %s not found\n%s" % (field_name, repr(self.fields)) - - def __delitem__(self, field_name): - "Deletes the field with the given field name; raises KeyError on failure" - for i, field in enumerate(self.fields): - if field.field_name == field_name: - del self.fields[i] - return - raise KeyError, "Field %s not found" % field_name - - def check_permissions(self, user): - """Confirms user has required permissions to use this manipulator; raises - PermissionDenied on failure.""" - if self.required_permission is None: - return - if user.has_perm(self.required_permission): - return - raise PermissionDenied - - def prepare(self, new_data): - """ - Makes any necessary preparations to new_data, in place, before data has - been validated. - """ - for field in self.fields: - field.prepare(new_data) - - def get_validation_errors(self, new_data): - "Returns dictionary mapping field_names to error-message lists" - errors = {} - self.prepare(new_data) - for field in self.fields: - errors.update(field.get_validation_errors(new_data)) - val_name = 'validate_%s' % field.field_name - if hasattr(self, val_name): - val = getattr(self, val_name) - try: - field.run_validator(new_data, val) - except (validators.ValidationError, validators.CriticalValidationError), e: - errors.setdefault(field.field_name, []).extend(e.messages) - -# if field.is_required and not new_data.get(field.field_name, False): -# errors.setdefault(field.field_name, []).append(gettext_lazy('This field is required.')) -# continue -# try: -# validator_list = field.validator_list -# if hasattr(self, 'validate_%s' % field.field_name): -# validator_list.append(getattr(self, 'validate_%s' % field.field_name)) -# for validator in validator_list: -# if field.is_required or new_data.get(field.field_name, False) or hasattr(validator, 'always_test'): -# try: -# if hasattr(field, 'requires_data_list'): -# validator(new_data.getlist(field.field_name), new_data) -# else: -# validator(new_data.get(field.field_name, ''), new_data) -# except validators.ValidationError, e: -# errors.setdefault(field.field_name, []).extend(e.messages) -# # If a CriticalValidationError is raised, ignore any other ValidationErrors -# # for this particular field -# except validators.CriticalValidationError, e: -# errors.setdefault(field.field_name, []).extend(e.messages) - return errors - - def save(self, new_data): - "Saves the changes and returns the new object" - # changes is a dictionary-like object keyed by field_name - raise NotImplementedError - - def do_html2python(self, new_data): - """ - Convert the data from HTML data types to Python datatypes, changing the - object in place. This happens after validation but before storage. This - must happen after validation because html2python functions aren't - expected to deal with invalid input. - """ - for field in self.fields: - field.convert_post_data(new_data) - -class FormWrapper(object): - """ - A wrapper linking a Manipulator to the template system. - This allows dictionary-style lookups of formfields. It also handles feeding - prepopulated data and validation error messages to the formfield objects. - """ - def __init__(self, manipulator, data=None, error_dict=None, edit_inline=True): - self.manipulator = manipulator - if data is None: - data = {} - if error_dict is None: - error_dict = {} - self.data = data - self.error_dict = error_dict - self._inline_collections = None - self.edit_inline = edit_inline - - def __repr__(self): - return repr(self.__dict__) - - def __getitem__(self, key): - for field in self.manipulator.fields: - if field.field_name == key: - data = field.extract_data(self.data) - return FormFieldWrapper(field, data, self.error_dict.get(field.field_name, [])) - if self.edit_inline: - self.fill_inline_collections() - for inline_collection in self._inline_collections: - if inline_collection.name == key: - return inline_collection - raise KeyError, "Could not find Formfield or InlineObjectCollection named %r" % key - - def fill_inline_collections(self): - if not self._inline_collections: - ic = [] - related_objects = self.manipulator.get_related_objects() - for rel_obj in related_objects: - data = rel_obj.extract_data(self.data) - inline_collection = InlineObjectCollection(self.manipulator, rel_obj, data, self.error_dict) - ic.append(inline_collection) - self._inline_collections = ic - - def has_errors(self): - return self.error_dict != {} - - def _get_fields(self): - try: - return self._fields - except AttributeError: - self._fields = [self.__getitem__(field.field_name) for field in self.manipulator.fields] - return self._fields - - fields = property(_get_fields) - -class FormFieldWrapper(object): - "A bridge between the template system and an individual form field. Used by FormWrapper." - def __init__(self, formfield, data, error_list): - self.formfield, self.data, self.error_list = formfield, data, error_list - self.field_name = self.formfield.field_name # for convenience in templates - - def __str__(self): - "Renders the field" - return str(self.formfield.render(self.data)) - - def __repr__(self): - return '' % self.formfield.field_name - - def field_list(self): - """ - Like __str__(), but returns a list. Use this when the field's render() - method returns a list. - """ - return self.formfield.render(self.data) - - def errors(self): - return self.error_list - - def html_error_list(self): - if self.errors(): - return '
  • %s
' % '
  • '.join([escape(e) for e in self.errors()]) - else: - return '' - - def get_id(self): - return self.formfield.get_id() - -class FormFieldCollection(FormFieldWrapper): - "A utility class that gives the template access to a dict of FormFieldWrappers" - def __init__(self, formfield_dict): - self.formfield_dict = formfield_dict - - def __str__(self): - return str(self.formfield_dict) - - def __getitem__(self, template_key): - "Look up field by template key; raise KeyError on failure" - return self.formfield_dict[template_key] - - def __repr__(self): - return "" % self.formfield_dict - - def errors(self): - "Returns list of all errors in this collection's formfields" - errors = [] - for field in self.formfield_dict.values(): - if hasattr(field, 'errors'): - errors.extend(field.errors()) - return errors - - def has_errors(self): - return bool(len(self.errors())) - - def html_combined_error_list(self): - return ''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')]) - -class InlineObjectCollection(object): - "An object that acts like a sparse list of form field collections." - def __init__(self, parent_manipulator, rel_obj, data, errors): - self.parent_manipulator = parent_manipulator - self.rel_obj = rel_obj - self.data = data - self.errors = errors - self._collections = None - self.name = rel_obj.name - - def __len__(self): - self.fill() - return self._collections.__len__() - - def __getitem__(self, k): - self.fill() - return self._collections.__getitem__(k) - - def __setitem__(self, k, v): - self.fill() - return self._collections.__setitem__(k,v) - - def __delitem__(self, k): - self.fill() - return self._collections.__delitem__(k) - - def __iter__(self): - self.fill() - return iter(self._collections.values()) - - def items(self): - self.fill() - return self._collections.items() - - def fill(self): - if self._collections: - return - else: - var_name = self.rel_obj.opts.object_name.lower() - collections = {} - orig = None - if hasattr(self.parent_manipulator, 'original_object'): - orig = self.parent_manipulator.original_object - orig_list = self.rel_obj.get_list(orig) - - for i, instance in enumerate(orig_list): - collection = {'original': instance} - for f in self.rel_obj.editable_fields(): - for field_name in f.get_manipulator_field_names(''): - full_field_name = '%s.%d.%s' % (var_name, i, field_name) - field = self.parent_manipulator[full_field_name] - data = field.extract_data(self.data) - errors = self.errors.get(full_field_name, []) - collection[field_name] = FormFieldWrapper(field, data, errors) - collections[i] = FormFieldCollection(collection) - self._collections = collections - - -class FormField(object): - """Abstract class representing a form field. - - Classes that extend FormField should define the following attributes: - field_name - The field's name for use by programs. - validator_list - A list of validation tests (callback functions) that the data for - this field must pass in order to be added or changed. - is_required - A Boolean. Is it a required field? - Subclasses should also implement a render(data) method, which is responsible - for rending the form field in XHTML. - """ - def __str__(self): - return self.render('') - - def __repr__(self): - return 'FormField "%s"' % self.field_name - - def prepare(self, new_data): - "Hook for doing something to new_data (in place) before validation." - pass - - def html2python(data): - "Hook for converting an HTML datatype (e.g. 'on' for checkboxes) to a Python type" - return data - html2python = staticmethod(html2python) - - def render(self, data): - raise NotImplementedError - - def get_member_name(self): - if hasattr(self, 'member_name'): - return self.member_name - else: - return self.field_name - - def extract_data(self, data_dict): - if hasattr(self, 'requires_data_list') and hasattr(data_dict, 'getlist'): - data = data_dict.getlist(self.get_member_name()) - else: - data = data_dict.get(self.get_member_name(), None) - if data is None: - data = '' - return data - - def convert_post_data(self, new_data): - name = self.get_member_name() - if new_data.has_key(self.field_name): - d = new_data.getlist(self.field_name) - try: - converted_data = [self.__class__.html2python(data) for data in d] - except ValueError: - converted_data = d - new_data.setlist(name, converted_data) - else: - try: - #individual fields deal with None values themselves - new_data.setlist(name, [self.__class__.html2python(None)]) - except EmptyValue: - new_data.setlist(name, []) - - - def run_validator(self, new_data, validator): - if self.is_required or new_data.get(self.field_name, False) or hasattr(validator, 'always_test'): - if hasattr(self, 'requires_data_list'): - validator(new_data.getlist(self.field_name), new_data) - else: - validator(new_data.get(self.field_name, ''), new_data) - - def get_validation_errors(self, new_data): - errors = {} - if self.is_required and not new_data.get(self.field_name, False): - errors.setdefault(self.field_name, []).append(gettext('This field is required.')) - return errors - try: - for validator in self.validator_list: - try: - self.run_validator(new_data, validator) - except validators.ValidationError, e: - errors.setdefault(self.field_name, []).extend(e.messages) - # If a CriticalValidationError is raised, ignore any other ValidationErrors - # for this particular field - except validators.CriticalValidationError, e: - errors.setdefault(self.field_name, []).extend(e.messages) - return errors - - def get_id(self): - "Returns the HTML 'id' attribute for this form field." - return FORM_FIELD_ID_PREFIX + self.field_name - -#################### -# GENERIC WIDGETS # -#################### - -class TextField(FormField): - input_type = "text" - def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None, member_name=None): - if validator_list is None: validator_list = [] - self.field_name = field_name - self.length, self.maxlength = length, maxlength - self.is_required = is_required - self.validator_list = [self.isValidLength, self.hasNoNewlines] + validator_list - if member_name != None: - self.member_name = member_name - - def isValidLength(self, data, form): - if data and self.maxlength and len(data.decode(settings.DEFAULT_CHARSET)) > self.maxlength: - raise validators.ValidationError, ngettext("Ensure your text is less than %s character.", - "Ensure your text is less than %s characters.", self.maxlength) % self.maxlength - - def hasNoNewlines(self, data, form): - if data and '\n' in data: - raise validators.ValidationError, gettext("Line breaks are not allowed here.") - - def render(self, data): - if data is None: - data = '' - maxlength = '' - if self.maxlength: - maxlength = 'maxlength="%s" ' % self.maxlength - if isinstance(data, unicode): - data = data.encode(settings.DEFAULT_CHARSET) - return '' % \ - (self.input_type, self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', - self.field_name, self.length, escape(data), maxlength) - - def html2python(data): - return data - html2python = staticmethod(html2python) - -class PasswordField(TextField): - input_type = "password" - -class LargeTextField(TextField): - def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=None, maxlength=None): - if validator_list is None: validator_list = [] - self.field_name = field_name - self.rows, self.cols, self.is_required = rows, cols, is_required - self.validator_list = validator_list[:] - if maxlength: - self.validator_list.append(self.isValidLength) - self.maxlength = maxlength - - def render(self, data): - if data is None: - data = '' - if isinstance(data, unicode): - data = data.encode(settings.DEFAULT_CHARSET) - return '' % \ - (self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', - self.field_name, self.rows, self.cols, escape(data)) - -class HiddenField(FormField): - def __init__(self, field_name, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - self.field_name, self.is_required = field_name, is_required - self.validator_list = validator_list[:] - - def render(self, data): - return '' % \ - (self.get_id(), self.field_name, escape(data)) - -class CheckboxField(FormField): - def __init__(self, field_name, checked_by_default=False, validator_list=None, is_required=False): - if validator_list is None: validator_list = [] - self.field_name = field_name - self.checked_by_default = checked_by_default - self.is_required = is_required - self.validator_list = validator_list[:] - - def render(self, data): - checked_html = '' - if data or (data is '' and self.checked_by_default): - checked_html = ' checked="checked"' - return '' % \ - (self.get_id(), self.__class__.__name__, - self.field_name, checked_html) - - def html2python(data): - "Convert value from browser ('on' or '') to a Python boolean" - if data == 'on': - return True - return False - html2python = staticmethod(html2python) - -class SelectField(FormField): - def __init__(self, field_name, choices=None, size=1, is_required=False, validator_list=None, member_name=None): - if validator_list is None: validator_list = [] - if choices is None: choices = [] - self.field_name = field_name - # choices is a list of (value, human-readable key) tuples because order matters - self.choices, self.size, self.is_required = choices, size, is_required - self.validator_list = [self.isValidChoice] + validator_list - if member_name != None: - self.member_name = member_name - - def render(self, data): - output = ['') - return '\n'.join(output) - - def isValidChoice(self, data, form): - str_data = str(data) - str_choices = [str(item[0]) for item in self.choices] - if str_data not in str_choices: - raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data': str_data, 'choices': str_choices} - -class NullSelectField(SelectField): - "This SelectField converts blank fields to None" - def html2python(data): - if not data: - return None - return data - html2python = staticmethod(html2python) - -class RadioSelectField(FormField): - def __init__(self, field_name, choices=None, ul_class='', is_required=False, validator_list=None, member_name=None): - if validator_list is None: validator_list = [] - if choices is None: choices = [] - self.field_name = field_name - # choices is a list of (value, human-readable key) tuples because order matters - self.choices, self.is_required = choices, is_required - self.validator_list = [self.isValidChoice] + validator_list - self.ul_class = ul_class - if member_name != None: - self.member_name = member_name - - def render(self, data): - """ - Returns a special object, RadioFieldRenderer, that is iterable *and* - has a default str() rendered output. - - This allows for flexible use in templates. You can just use the default - rendering: - - {{ field_name }} - - ...which will output the radio buttons in an unordered list. - Or, you can manually traverse each radio option for special layout: - - {% for option in field_name.field_list %} - {{ option.field }} {{ option.label }}
    - {% endfor %} - """ - class RadioFieldRenderer: - def __init__(self, datalist, ul_class): - self.datalist, self.ul_class = datalist, ul_class - def __str__(self): - "Default str() output for this radio field -- a
      " - output = ['' % (self.ul_class and ' class="%s"' % self.ul_class or '')] - output.extend(['
    • %s %s
    • ' % (d['field'], d['label']) for d in self.datalist]) - output.append('
    ') - return ''.join(output) - def __iter__(self): - for d in self.datalist: - yield d - def __len__(self): - return len(self.datalist) - datalist = [] - str_data = str(data) # normalize to string - for i, (value, display_name) in enumerate(self.choices): - selected_html = '' - if str(value) == str_data: - selected_html = ' checked="checked"' - datalist.append({ - 'value': value, - 'name': display_name, - 'field': '' % \ - (self.get_id() + '_' + str(i), self.field_name, value, selected_html), - 'label': '' % \ - (self.get_id() + '_' + str(i), display_name), - }) - return RadioFieldRenderer(datalist, self.ul_class) - - def isValidChoice(self, data, form): - str_data = str(data) - str_choices = [str(item[0]) for item in self.choices] - if str_data not in str_choices: - raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':str_data, 'choices':str_choices} - -class NullBooleanField(SelectField): - "This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None" - def __init__(self, field_name, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - SelectField.__init__(self, field_name, choices=[('1', 'Unknown'), ('2', 'Yes'), ('3', 'No')], - is_required=is_required, validator_list=validator_list) - - def render(self, data): - if data is None: data = '1' - elif data == True: data = '2' - elif data == False: data = '3' - return SelectField.render(self, data) - - def html2python(data): - return {None: None, '1': None, '2': True, '3': False}[data] - html2python = staticmethod(html2python) - -class SelectMultipleField(SelectField): - requires_data_list = True - def render(self, data): - output = ['') - return '\n'.join(output) - - def isValidChoice(self, field_data, all_data): - # data is something like ['1', '2', '3'] - str_choices = [str(item[0]) for item in self.choices] - for val in map(str, field_data): - if val not in str_choices: - raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':val, 'choices':str_choices} - - def html2python(data): - if data is None: - raise EmptyValue - return data - html2python = staticmethod(html2python) - -class CheckboxSelectMultipleField(SelectMultipleField): - """ - This has an identical interface to SelectMultipleField, except the rendered - widget is different. Instead of a es. - - Of course, that results in multiple form elements for the same "single" - field, so this class's prepare() method flattens the split data elements - back into the single list that validators, renderers and save() expect. - """ - requires_data_list = True - def __init__(self, field_name, choices=None, ul_class='', validator_list=None): - if validator_list is None: validator_list = [] - if choices is None: choices = [] - self.ul_class = ul_class - SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list) - - def prepare(self, new_data): - # new_data has "split" this field into several fields, so flatten it - # back into a single list. - data_list = [] - for value, readable_value in self.choices: - if new_data.get('%s%s' % (self.field_name, value), '') == 'on': - data_list.append(value) - new_data.setlist(self.field_name, data_list) - - def render(self, data): - output = ['' % (self.ul_class and ' class="%s"' % self.ul_class or '')] - str_data_list = map(str, data) # normalize to strings - for value, choice in self.choices: - checked_html = '' - if str(value) in str_data_list: - checked_html = ' checked="checked"' - field_name = '%s%s' % (self.field_name, value) - output.append('
  • ' % \ - (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, - self.get_id() + escape(value), choice)) - output.append('') - return '\n'.join(output) - -#################### -# FILE UPLOADS # -#################### - -class FileUploadField(FormField): - def __init__(self, field_name, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - self.field_name, self.is_required = field_name, is_required - self.validator_list = [self.isNonEmptyFile] + validator_list - - def isNonEmptyFile(self, field_data, all_data): - try: - content = field_data['content'] - except TypeError: - raise validators.CriticalValidationError, gettext("No file was submitted. Check the encoding type on the form.") - if not content: - raise validators.CriticalValidationError, gettext("The submitted file is empty.") - - def render(self, data): - return '' % \ - (self.get_id(), self.__class__.__name__, self.field_name) - - def html2python(data): - if data is None: - raise EmptyValue - return data - html2python = staticmethod(html2python) - -class ImageUploadField(FileUploadField): - "A FileUploadField that raises CriticalValidationError if the uploaded file isn't an image." - def __init__(self, *args, **kwargs): - FileUploadField.__init__(self, *args, **kwargs) - self.validator_list.insert(0, self.isValidImage) - - def isValidImage(self, field_data, all_data): - try: - validators.isValidImage(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - -#################### -# INTEGERS/FLOATS # -#################### - -class IntegerField(TextField): - def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None, member_name=None): - if validator_list is None: validator_list = [] - validator_list = [self.isInteger] + validator_list - if member_name is not None: - self.member_name = member_name - TextField.__init__(self, field_name, length, maxlength, is_required, validator_list) - - def isInteger(self, field_data, all_data): - try: - validators.isInteger(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - - def html2python(data): - if data == '' or data is None: - return None - return int(data) - html2python = staticmethod(html2python) - -class SmallIntegerField(IntegerField): - def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isSmallInteger] + validator_list - IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) - - def isSmallInteger(self, field_data, all_data): - if not -32768 <= int(field_data) <= 32767: - raise validators.CriticalValidationError, gettext("Enter a whole number between -32,768 and 32,767.") - -class PositiveIntegerField(IntegerField): - def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isPositive] + validator_list - IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) - - def isPositive(self, field_data, all_data): - if int(field_data) < 0: - raise validators.CriticalValidationError, gettext("Enter a positive number.") - -class PositiveSmallIntegerField(IntegerField): - def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isPositiveSmall] + validator_list - IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) - - def isPositiveSmall(self, field_data, all_data): - if not 0 <= int(field_data) <= 32767: - raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.") - -class FloatField(TextField): - def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - self.max_digits, self.decimal_places = max_digits, decimal_places - validator_list = [self.isValidFloat] + validator_list - TextField.__init__(self, field_name, max_digits+2, max_digits+2, is_required, validator_list) - - def isValidFloat(self, field_data, all_data): - v = validators.IsValidFloat(self.max_digits, self.decimal_places) - try: - v(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - - def html2python(data): - if data == '' or data is None: - return None - return float(data) - html2python = staticmethod(html2python) - -#################### -# DATES AND TIMES # -#################### - -class DatetimeField(TextField): - """A FormField that automatically converts its data to a datetime.datetime object. - The data should be in the format YYYY-MM-DD HH:MM:SS.""" - def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - self.field_name = field_name - self.length, self.maxlength = length, maxlength - self.is_required = is_required - self.validator_list = [validators.isValidANSIDatetime] + validator_list - - def html2python(data): - "Converts the field into a datetime.datetime object" - import datetime - try: - date, time = data.split() - y, m, d = date.split('-') - timebits = time.split(':') - h, mn = timebits[:2] - if len(timebits) > 2: - s = int(timebits[2]) - else: - s = 0 - return datetime.datetime(int(y), int(m), int(d), int(h), int(mn), s) - except ValueError: - return None - html2python = staticmethod(html2python) - -class DateField(TextField): - """A FormField that automatically converts its data to a datetime.date object. - The data should be in the format YYYY-MM-DD.""" - def __init__(self, field_name, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isValidDate] + validator_list - TextField.__init__(self, field_name, length=10, maxlength=10, - is_required=is_required, validator_list=validator_list) - - def isValidDate(self, field_data, all_data): - try: - validators.isValidANSIDate(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - - def html2python(data): - "Converts the field into a datetime.date object" - import time, datetime - try: - time_tuple = time.strptime(data, '%Y-%m-%d') - return datetime.date(*time_tuple[0:3]) - except (ValueError, TypeError): - return None - html2python = staticmethod(html2python) - -class TimeField(TextField): - """A FormField that automatically converts its data to a datetime.time object. - The data should be in the format HH:MM:SS or HH:MM:SS.mmmmmm.""" - def __init__(self, field_name, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isValidTime] + validator_list - TextField.__init__(self, field_name, length=8, maxlength=8, - is_required=is_required, validator_list=validator_list) - - def isValidTime(self, field_data, all_data): - try: - validators.isValidANSITime(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - - def html2python(data): - "Converts the field into a datetime.time object" - import time, datetime - try: - part_list = data.split('.') - try: - time_tuple = time.strptime(part_list[0], '%H:%M:%S') - except ValueError: # seconds weren't provided - time_tuple = time.strptime(part_list[0], '%H:%M') - t = datetime.time(*time_tuple[3:6]) - if (len(part_list) == 2): - t = t.replace(microsecond=int(part_list[1])) - return t - except (ValueError, TypeError, AttributeError): - return None - html2python = staticmethod(html2python) - -#################### -# INTERNET-RELATED # -#################### - -class EmailField(TextField): - "A convenience FormField for validating e-mail addresses" - def __init__(self, field_name, length=50, maxlength=75, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isValidEmail] + validator_list - TextField.__init__(self, field_name, length, maxlength=maxlength, - is_required=is_required, validator_list=validator_list) - - def isValidEmail(self, field_data, all_data): - try: - validators.isValidEmail(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - -class URLField(TextField): - "A convenience FormField for validating URLs" - def __init__(self, field_name, length=50, maxlength=200, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isValidURL] + validator_list - TextField.__init__(self, field_name, length=length, maxlength=maxlength, - is_required=is_required, validator_list=validator_list) - - def isValidURL(self, field_data, all_data): - try: - validators.isValidURL(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - -class IPAddressField(TextField): - def __init__(self, field_name, length=15, maxlength=15, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isValidIPAddress] + validator_list - TextField.__init__(self, field_name, length=length, maxlength=maxlength, - is_required=is_required, validator_list=validator_list) - - def isValidIPAddress(self, field_data, all_data): - try: - validators.isValidIPAddress4(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - - def html2python(data): - return data or None - html2python = staticmethod(html2python) - -#################### -# MISCELLANEOUS # -#################### - -class FilePathField(SelectField): - "A SelectField whose choices are the files in a given directory." - def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None): - import os - from django.db.models import BLANK_CHOICE_DASH - if match is not None: - import re - match_re = re.compile(match) - choices = not is_required and BLANK_CHOICE_DASH[:] or [] - if recursive: - for root, dirs, files in os.walk(path): - for f in files: - if match is None or match_re.search(f): - choices.append((os.path.join(root, f), f)) - else: - try: - for f in os.listdir(path): - full_file = os.path.join(path, f) - if os.path.isfile(full_file) and (match is None or match_re.search(f)): - choices.append((full_file, f)) - except OSError: - pass - SelectField.__init__(self, field_name, choices, 1, is_required, validator_list) - -class PhoneNumberField(TextField): - "A convenience FormField for validating phone numbers (e.g. '630-555-1234')" - def __init__(self, field_name, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isValidPhone] + validator_list - TextField.__init__(self, field_name, length=12, maxlength=12, - is_required=is_required, validator_list=validator_list) - - def isValidPhone(self, field_data, all_data): - try: - validators.isValidPhone(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - -class USStateField(TextField): - "A convenience FormField for validating U.S. states (e.g. 'IL')" - def __init__(self, field_name, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isValidUSState] + validator_list - TextField.__init__(self, field_name, length=2, maxlength=2, - is_required=is_required, validator_list=validator_list) - - def isValidUSState(self, field_data, all_data): - try: - validators.isValidUSState(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - - def html2python(data): - return data.upper() # Should always be stored in upper case - html2python = staticmethod(html2python) - -class CommaSeparatedIntegerField(TextField): - "A convenience FormField for validating comma-separated integer fields" - def __init__(self, field_name, maxlength=None, is_required=False, validator_list=None): - if validator_list is None: validator_list = [] - validator_list = [self.isCommaSeparatedIntegerList] + validator_list - TextField.__init__(self, field_name, length=20, maxlength=maxlength, - is_required=is_required, validator_list=validator_list) - - def isCommaSeparatedIntegerList(self, field_data, all_data): - try: - validators.isCommaSeparatedIntegerList(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages - - def render(self, data): - if data is None: - data = '' - elif isinstance(data, (list, tuple)): - data = ','.join(data) - return super(CommaSeparatedIntegerField, self).render(data) - -class RawIdAdminField(CommaSeparatedIntegerField): - def html2python(data): - if data: - return data.split(',') - else: - return [] - html2python = staticmethod(html2python) - -class XMLLargeTextField(LargeTextField): - """ - A LargeTextField with an XML validator. The schema_path argument is the - full path to a Relax NG compact schema to validate against. - """ - def __init__(self, field_name, schema_path, **kwargs): - self.schema_path = schema_path - kwargs.setdefault('validator_list', []).insert(0, self.isValidXML) - LargeTextField.__init__(self, field_name, **kwargs) - - def isValidXML(self, field_data, all_data): - v = validators.RelaxNGCompact(self.schema_path) - try: - v(field_data, all_data) - except validators.ValidationError, e: - raise validators.CriticalValidationError, e.messages +from django.oldforms import * diff --git a/django/http/__init__.py b/django/http/__init__.py index bb0e973aae..48f10329fd 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -208,7 +208,7 @@ class HttpResponse(object): if path is not None: self.cookies[key]['path'] = path if domain is not None: - self.cookies[key]['domain'] = path + self.cookies[key]['domain'] = domain self.cookies[key]['expires'] = 0 self.cookies[key]['max-age'] = 0 diff --git a/django/newforms/__init__.py b/django/newforms/__init__.py index 2a472d7b39..0d9c68f9e0 100644 --- a/django/newforms/__init__.py +++ b/django/newforms/__init__.py @@ -13,16 +13,5 @@ TODO: from util import ValidationError from widgets import * from fields import * -from forms import Form - -########################## -# DATABASE API SHORTCUTS # -########################## - -def form_for_model(model): - "Returns a Form instance for the given Django model class." - raise NotImplementedError - -def form_for_fields(field_list): - "Returns a Form instance for the given list of Django database field instances." - raise NotImplementedError +from forms import * +from models import * diff --git a/django/newforms/extras/__init__.py b/django/newforms/extras/__init__.py new file mode 100644 index 0000000000..a7f6a9b3f6 --- /dev/null +++ b/django/newforms/extras/__init__.py @@ -0,0 +1 @@ +from widgets import * diff --git a/django/newforms/extras/widgets.py b/django/newforms/extras/widgets.py new file mode 100644 index 0000000000..1011934fb8 --- /dev/null +++ b/django/newforms/extras/widgets.py @@ -0,0 +1,59 @@ +""" +Extra HTML Widget classes +""" + +from django.newforms.widgets import Widget, Select +from django.utils.dates import MONTHS +import datetime + +__all__ = ('SelectDateWidget',) + +class SelectDateWidget(Widget): + """ + A Widget that splits date input into three ' % (flatatt(final_attrs), escape(value)) class CheckboxInput(Widget): + def __init__(self, attrs=None, check_test=bool): + # check_test is a callable that takes a value and returns True + # if the checkbox should be checked for that value. + self.attrs = attrs or {} + self.check_test = check_test + def render(self, name, value, attrs=None): final_attrs = self.build_attrs(attrs, type='checkbox', name=name) - if value: final_attrs['checked'] = 'checked' + try: + result = self.check_test(value) + except: # Silently catch exceptions + result = False + if result: + final_attrs['checked'] = 'checked' + if value not in ('', True, False, None): + final_attrs['value'] = smart_unicode(value) # Only add the 'value' attribute if a value is non-empty. return u'' % flatatt(final_attrs) class Select(Widget): @@ -91,7 +135,6 @@ class Select(Widget): return u'\n'.join(output) class SelectMultiple(Widget): - requires_data_list = True def __init__(self, attrs=None, choices=()): # choices can be any iterable self.attrs = attrs or {} @@ -109,36 +152,48 @@ class SelectMultiple(Widget): output.append(u'') return u'\n'.join(output) -class RadioInput(object): - "An object used by RadioFieldRenderer that represents a single ." - def __init__(self, name, value, attrs, choice): - self.name, self.value = name, value - self.attrs = attrs or {} - self.choice_value, self.choice_label = choice + def value_from_datadict(self, data, name): + if isinstance(data, MultiValueDict): + return data.getlist(name) + return data.get(name, None) - def __str__(self): +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 + self.attrs = attrs + self.choice_value, self.choice_label = choice + self.index = index + + def __unicode__(self): return u'' % (self.tag(), self.choice_label) def is_checked(self): return self.value == smart_unicode(self.choice_value) def tag(self): + if self.attrs.has_key('id'): + self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value) if self.is_checked(): 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 self.choices = choices def __iter__(self): - for choice in self.choices: - yield RadioInput(self.name, self.value, self.attrs, choice) + for i, choice in enumerate(self.choices): + yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i) - def __str__(self): + def __getitem__(self, idx): + choice = self.choices[idx] # Let the IndexError propogate + return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx) + + 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]) @@ -147,7 +202,36 @@ class RadioSelect(Select): "Returns a RadioFieldRenderer instance rather than a Unicode string." if value is None: value = '' str_value = smart_unicode(value) # Normalize to string. + attrs = attrs or {} return RadioFieldRenderer(name, str_value, attrs, list(chain(self.choices, choices))) -class CheckboxSelectMultiple(Widget): - pass + def id_for_label(self, id_): + # RadioSelect is represented by multiple fields, + # each of which has a distinct ID. The IDs are made distinct by a "_X" + # suffix, where X is the zero-based index of the radio field. Thus, + # the label for a RadioSelect should reference the first one ('_0'). + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) + +class CheckboxSelectMultiple(SelectMultiple): + def render(self, name, value, attrs=None, choices=()): + if value is None: value = [] + final_attrs = self.build_attrs(attrs, name=name) + output = [u'
        '] + str_values = set([smart_unicode(v) for v in value]) # Normalize to strings. + cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values) + for option_value, option_label in chain(self.choices, choices): + option_value = smart_unicode(option_value) + rendered_cb = cb.render(name, option_value) + output.append(u'
      • ' % (rendered_cb, escape(smart_unicode(option_label)))) + output.append(u'
      ') + return u'\n'.join(output) + + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py new file mode 100644 index 0000000000..0b9ac05edb --- /dev/null +++ b/django/oldforms/__init__.py @@ -0,0 +1,1008 @@ +from django.core import validators +from django.core.exceptions import PermissionDenied +from django.utils.html import escape +from django.conf import settings +from django.utils.translation import gettext, ngettext + +FORM_FIELD_ID_PREFIX = 'id_' + +class EmptyValue(Exception): + "This is raised when empty data is provided" + pass + +class Manipulator(object): + # List of permission strings. User must have at least one to manipulate. + # None means everybody has permission. + required_permission = '' + + def __init__(self): + # List of FormField objects + self.fields = [] + + def __getitem__(self, field_name): + "Looks up field by field name; raises KeyError on failure" + for field in self.fields: + if field.field_name == field_name: + return field + raise KeyError, "Field %s not found\n%s" % (field_name, repr(self.fields)) + + def __delitem__(self, field_name): + "Deletes the field with the given field name; raises KeyError on failure" + for i, field in enumerate(self.fields): + if field.field_name == field_name: + del self.fields[i] + return + raise KeyError, "Field %s not found" % field_name + + def check_permissions(self, user): + """Confirms user has required permissions to use this manipulator; raises + PermissionDenied on failure.""" + if self.required_permission is None: + return + if user.has_perm(self.required_permission): + return + raise PermissionDenied + + def prepare(self, new_data): + """ + Makes any necessary preparations to new_data, in place, before data has + been validated. + """ + for field in self.fields: + field.prepare(new_data) + + def get_validation_errors(self, new_data): + "Returns dictionary mapping field_names to error-message lists" + errors = {} + self.prepare(new_data) + for field in self.fields: + errors.update(field.get_validation_errors(new_data)) + val_name = 'validate_%s' % field.field_name + if hasattr(self, val_name): + val = getattr(self, val_name) + try: + field.run_validator(new_data, val) + except (validators.ValidationError, validators.CriticalValidationError), e: + errors.setdefault(field.field_name, []).extend(e.messages) + +# if field.is_required and not new_data.get(field.field_name, False): +# errors.setdefault(field.field_name, []).append(gettext_lazy('This field is required.')) +# continue +# try: +# validator_list = field.validator_list +# if hasattr(self, 'validate_%s' % field.field_name): +# validator_list.append(getattr(self, 'validate_%s' % field.field_name)) +# for validator in validator_list: +# if field.is_required or new_data.get(field.field_name, False) or hasattr(validator, 'always_test'): +# try: +# if hasattr(field, 'requires_data_list'): +# validator(new_data.getlist(field.field_name), new_data) +# else: +# validator(new_data.get(field.field_name, ''), new_data) +# except validators.ValidationError, e: +# errors.setdefault(field.field_name, []).extend(e.messages) +# # If a CriticalValidationError is raised, ignore any other ValidationErrors +# # for this particular field +# except validators.CriticalValidationError, e: +# errors.setdefault(field.field_name, []).extend(e.messages) + return errors + + def save(self, new_data): + "Saves the changes and returns the new object" + # changes is a dictionary-like object keyed by field_name + raise NotImplementedError + + def do_html2python(self, new_data): + """ + Convert the data from HTML data types to Python datatypes, changing the + object in place. This happens after validation but before storage. This + must happen after validation because html2python functions aren't + expected to deal with invalid input. + """ + for field in self.fields: + field.convert_post_data(new_data) + +class FormWrapper(object): + """ + A wrapper linking a Manipulator to the template system. + This allows dictionary-style lookups of formfields. It also handles feeding + prepopulated data and validation error messages to the formfield objects. + """ + def __init__(self, manipulator, data=None, error_dict=None, edit_inline=True): + self.manipulator = manipulator + if data is None: + data = {} + if error_dict is None: + error_dict = {} + self.data = data + self.error_dict = error_dict + self._inline_collections = None + self.edit_inline = edit_inline + + def __repr__(self): + return repr(self.__dict__) + + def __getitem__(self, key): + for field in self.manipulator.fields: + if field.field_name == key: + data = field.extract_data(self.data) + return FormFieldWrapper(field, data, self.error_dict.get(field.field_name, [])) + if self.edit_inline: + self.fill_inline_collections() + for inline_collection in self._inline_collections: + if inline_collection.name == key: + return inline_collection + raise KeyError, "Could not find Formfield or InlineObjectCollection named %r" % key + + def fill_inline_collections(self): + if not self._inline_collections: + ic = [] + related_objects = self.manipulator.get_related_objects() + for rel_obj in related_objects: + data = rel_obj.extract_data(self.data) + inline_collection = InlineObjectCollection(self.manipulator, rel_obj, data, self.error_dict) + ic.append(inline_collection) + self._inline_collections = ic + + def has_errors(self): + return self.error_dict != {} + + def _get_fields(self): + try: + return self._fields + except AttributeError: + self._fields = [self.__getitem__(field.field_name) for field in self.manipulator.fields] + return self._fields + + fields = property(_get_fields) + +class FormFieldWrapper(object): + "A bridge between the template system and an individual form field. Used by FormWrapper." + def __init__(self, formfield, data, error_list): + self.formfield, self.data, self.error_list = formfield, data, error_list + self.field_name = self.formfield.field_name # for convenience in templates + + def __str__(self): + "Renders the field" + return str(self.formfield.render(self.data)) + + def __repr__(self): + return '' % self.formfield.field_name + + def field_list(self): + """ + Like __str__(), but returns a list. Use this when the field's render() + method returns a list. + """ + return self.formfield.render(self.data) + + def errors(self): + return self.error_list + + def html_error_list(self): + if self.errors(): + return '
      • %s
      ' % '
    • '.join([escape(e) for e in self.errors()]) + else: + return '' + + def get_id(self): + return self.formfield.get_id() + +class FormFieldCollection(FormFieldWrapper): + "A utility class that gives the template access to a dict of FormFieldWrappers" + def __init__(self, formfield_dict): + self.formfield_dict = formfield_dict + + def __str__(self): + return str(self.formfield_dict) + + def __getitem__(self, template_key): + "Look up field by template key; raise KeyError on failure" + return self.formfield_dict[template_key] + + def __repr__(self): + return "" % self.formfield_dict + + def errors(self): + "Returns list of all errors in this collection's formfields" + errors = [] + for field in self.formfield_dict.values(): + if hasattr(field, 'errors'): + errors.extend(field.errors()) + return errors + + def has_errors(self): + return bool(len(self.errors())) + + def html_combined_error_list(self): + return ''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')]) + +class InlineObjectCollection(object): + "An object that acts like a sparse list of form field collections." + def __init__(self, parent_manipulator, rel_obj, data, errors): + self.parent_manipulator = parent_manipulator + self.rel_obj = rel_obj + self.data = data + self.errors = errors + self._collections = None + self.name = rel_obj.name + + def __len__(self): + self.fill() + return self._collections.__len__() + + def __getitem__(self, k): + self.fill() + return self._collections.__getitem__(k) + + def __setitem__(self, k, v): + self.fill() + return self._collections.__setitem__(k,v) + + def __delitem__(self, k): + self.fill() + return self._collections.__delitem__(k) + + def __iter__(self): + self.fill() + return iter(self._collections.values()) + + def items(self): + self.fill() + return self._collections.items() + + def fill(self): + if self._collections: + return + else: + var_name = self.rel_obj.opts.object_name.lower() + collections = {} + orig = None + if hasattr(self.parent_manipulator, 'original_object'): + orig = self.parent_manipulator.original_object + orig_list = self.rel_obj.get_list(orig) + + for i, instance in enumerate(orig_list): + collection = {'original': instance} + for f in self.rel_obj.editable_fields(): + for field_name in f.get_manipulator_field_names(''): + full_field_name = '%s.%d.%s' % (var_name, i, field_name) + field = self.parent_manipulator[full_field_name] + data = field.extract_data(self.data) + errors = self.errors.get(full_field_name, []) + collection[field_name] = FormFieldWrapper(field, data, errors) + collections[i] = FormFieldCollection(collection) + self._collections = collections + + +class FormField(object): + """Abstract class representing a form field. + + Classes that extend FormField should define the following attributes: + field_name + The field's name for use by programs. + validator_list + A list of validation tests (callback functions) that the data for + this field must pass in order to be added or changed. + is_required + A Boolean. Is it a required field? + Subclasses should also implement a render(data) method, which is responsible + for rending the form field in XHTML. + """ + def __str__(self): + return self.render('') + + def __repr__(self): + return 'FormField "%s"' % self.field_name + + def prepare(self, new_data): + "Hook for doing something to new_data (in place) before validation." + pass + + def html2python(data): + "Hook for converting an HTML datatype (e.g. 'on' for checkboxes) to a Python type" + return data + html2python = staticmethod(html2python) + + def render(self, data): + raise NotImplementedError + + def get_member_name(self): + if hasattr(self, 'member_name'): + return self.member_name + else: + return self.field_name + + def extract_data(self, data_dict): + if hasattr(self, 'requires_data_list') and hasattr(data_dict, 'getlist'): + data = data_dict.getlist(self.get_member_name()) + else: + data = data_dict.get(self.get_member_name(), None) + if data is None: + data = '' + return data + + def convert_post_data(self, new_data): + name = self.get_member_name() + if new_data.has_key(self.field_name): + d = new_data.getlist(self.field_name) + try: + converted_data = [self.__class__.html2python(data) for data in d] + except ValueError: + converted_data = d + new_data.setlist(name, converted_data) + else: + try: + #individual fields deal with None values themselves + new_data.setlist(name, [self.__class__.html2python(None)]) + except EmptyValue: + new_data.setlist(name, []) + + + def run_validator(self, new_data, validator): + if self.is_required or new_data.get(self.field_name, False) or hasattr(validator, 'always_test'): + if hasattr(self, 'requires_data_list'): + validator(new_data.getlist(self.field_name), new_data) + else: + validator(new_data.get(self.field_name, ''), new_data) + + def get_validation_errors(self, new_data): + errors = {} + if self.is_required and not new_data.get(self.field_name, False): + errors.setdefault(self.field_name, []).append(gettext('This field is required.')) + return errors + try: + for validator in self.validator_list: + try: + self.run_validator(new_data, validator) + except validators.ValidationError, e: + errors.setdefault(self.field_name, []).extend(e.messages) + # If a CriticalValidationError is raised, ignore any other ValidationErrors + # for this particular field + except validators.CriticalValidationError, e: + errors.setdefault(self.field_name, []).extend(e.messages) + return errors + + def get_id(self): + "Returns the HTML 'id' attribute for this form field." + return FORM_FIELD_ID_PREFIX + self.field_name + +#################### +# GENERIC WIDGETS # +#################### + +class TextField(FormField): + input_type = "text" + def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.length, self.maxlength = length, maxlength + self.is_required = is_required + self.validator_list = [self.isValidLength, self.hasNoNewlines] + validator_list + if member_name != None: + self.member_name = member_name + + def isValidLength(self, data, form): + if data and self.maxlength and len(data.decode(settings.DEFAULT_CHARSET)) > self.maxlength: + raise validators.ValidationError, ngettext("Ensure your text is less than %s character.", + "Ensure your text is less than %s characters.", self.maxlength) % self.maxlength + + def hasNoNewlines(self, data, form): + if data and '\n' in data: + raise validators.ValidationError, gettext("Line breaks are not allowed here.") + + def render(self, data): + if data is None: + data = '' + maxlength = '' + if self.maxlength: + maxlength = 'maxlength="%s" ' % self.maxlength + if isinstance(data, unicode): + data = data.encode(settings.DEFAULT_CHARSET) + return '' % \ + (self.input_type, self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', + self.field_name, self.length, escape(data), maxlength) + + def html2python(data): + return data + html2python = staticmethod(html2python) + +class PasswordField(TextField): + input_type = "password" + +class LargeTextField(TextField): + def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=None, maxlength=None): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.rows, self.cols, self.is_required = rows, cols, is_required + self.validator_list = validator_list[:] + if maxlength: + self.validator_list.append(self.isValidLength) + self.maxlength = maxlength + + def render(self, data): + if data is None: + data = '' + if isinstance(data, unicode): + data = data.encode(settings.DEFAULT_CHARSET) + return '' % \ + (self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', + self.field_name, self.rows, self.cols, escape(data)) + +class HiddenField(FormField): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.field_name, self.is_required = field_name, is_required + self.validator_list = validator_list[:] + + def render(self, data): + return '' % \ + (self.get_id(), self.field_name, escape(data)) + +class CheckboxField(FormField): + def __init__(self, field_name, checked_by_default=False, validator_list=None, is_required=False): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.checked_by_default = checked_by_default + self.is_required = is_required + self.validator_list = validator_list[:] + + def render(self, data): + checked_html = '' + if data or (data is '' and self.checked_by_default): + checked_html = ' checked="checked"' + return '' % \ + (self.get_id(), self.__class__.__name__, + self.field_name, checked_html) + + def html2python(data): + "Convert value from browser ('on' or '') to a Python boolean" + if data == 'on': + return True + return False + html2python = staticmethod(html2python) + +class SelectField(FormField): + def __init__(self, field_name, choices=None, size=1, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + if choices is None: choices = [] + self.field_name = field_name + # choices is a list of (value, human-readable key) tuples because order matters + self.choices, self.size, self.is_required = choices, size, is_required + self.validator_list = [self.isValidChoice] + validator_list + if member_name != None: + self.member_name = member_name + + def render(self, data): + output = ['') + return '\n'.join(output) + + def isValidChoice(self, data, form): + str_data = str(data) + str_choices = [str(item[0]) for item in self.choices] + if str_data not in str_choices: + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data': str_data, 'choices': str_choices} + +class NullSelectField(SelectField): + "This SelectField converts blank fields to None" + def html2python(data): + if not data: + return None + return data + html2python = staticmethod(html2python) + +class RadioSelectField(FormField): + def __init__(self, field_name, choices=None, ul_class='', is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + if choices is None: choices = [] + self.field_name = field_name + # choices is a list of (value, human-readable key) tuples because order matters + self.choices, self.is_required = choices, is_required + self.validator_list = [self.isValidChoice] + validator_list + self.ul_class = ul_class + if member_name != None: + self.member_name = member_name + + def render(self, data): + """ + Returns a special object, RadioFieldRenderer, that is iterable *and* + has a default str() rendered output. + + This allows for flexible use in templates. You can just use the default + rendering: + + {{ field_name }} + + ...which will output the radio buttons in an unordered list. + Or, you can manually traverse each radio option for special layout: + + {% for option in field_name.field_list %} + {{ option.field }} {{ option.label }}
      + {% endfor %} + """ + class RadioFieldRenderer: + def __init__(self, datalist, ul_class): + self.datalist, self.ul_class = datalist, ul_class + def __str__(self): + "Default str() output for this radio field -- a
        " + output = ['' % (self.ul_class and ' class="%s"' % self.ul_class or '')] + output.extend(['
      • %s %s
      • ' % (d['field'], d['label']) for d in self.datalist]) + output.append('
      ') + return ''.join(output) + def __iter__(self): + for d in self.datalist: + yield d + def __len__(self): + return len(self.datalist) + datalist = [] + str_data = str(data) # normalize to string + for i, (value, display_name) in enumerate(self.choices): + selected_html = '' + if str(value) == str_data: + selected_html = ' checked="checked"' + datalist.append({ + 'value': value, + 'name': display_name, + 'field': '' % \ + (self.get_id() + '_' + str(i), self.field_name, value, selected_html), + 'label': '' % \ + (self.get_id() + '_' + str(i), display_name), + }) + return RadioFieldRenderer(datalist, self.ul_class) + + def isValidChoice(self, data, form): + str_data = str(data) + str_choices = [str(item[0]) for item in self.choices] + if str_data not in str_choices: + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':str_data, 'choices':str_choices} + +class NullBooleanField(SelectField): + "This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + SelectField.__init__(self, field_name, choices=[('1', 'Unknown'), ('2', 'Yes'), ('3', 'No')], + is_required=is_required, validator_list=validator_list) + + def render(self, data): + if data is None: data = '1' + elif data == True: data = '2' + elif data == False: data = '3' + return SelectField.render(self, data) + + def html2python(data): + return {None: None, '1': None, '2': True, '3': False}[data] + html2python = staticmethod(html2python) + +class SelectMultipleField(SelectField): + requires_data_list = True + def render(self, data): + output = ['') + return '\n'.join(output) + + def isValidChoice(self, field_data, all_data): + # data is something like ['1', '2', '3'] + str_choices = [str(item[0]) for item in self.choices] + for val in map(str, field_data): + if val not in str_choices: + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':val, 'choices':str_choices} + + def html2python(data): + if data is None: + raise EmptyValue + return data + html2python = staticmethod(html2python) + +class CheckboxSelectMultipleField(SelectMultipleField): + """ + This has an identical interface to SelectMultipleField, except the rendered + widget is different. Instead of a es. + + Of course, that results in multiple form elements for the same "single" + field, so this class's prepare() method flattens the split data elements + back into the single list that validators, renderers and save() expect. + """ + requires_data_list = True + def __init__(self, field_name, choices=None, ul_class='', validator_list=None): + if validator_list is None: validator_list = [] + if choices is None: choices = [] + self.ul_class = ul_class + SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list) + + def prepare(self, new_data): + # new_data has "split" this field into several fields, so flatten it + # back into a single list. + data_list = [] + for value, readable_value in self.choices: + if new_data.get('%s%s' % (self.field_name, value), '') == 'on': + data_list.append(value) + new_data.setlist(self.field_name, data_list) + + def render(self, data): + output = ['' % (self.ul_class and ' class="%s"' % self.ul_class or '')] + str_data_list = map(str, data) # normalize to strings + for value, choice in self.choices: + checked_html = '' + if str(value) in str_data_list: + checked_html = ' checked="checked"' + field_name = '%s%s' % (self.field_name, value) + output.append('
    • ' % \ + (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, + self.get_id() + escape(value), choice)) + output.append('
    ') + return '\n'.join(output) + +#################### +# FILE UPLOADS # +#################### + +class FileUploadField(FormField): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.field_name, self.is_required = field_name, is_required + self.validator_list = [self.isNonEmptyFile] + validator_list + + def isNonEmptyFile(self, field_data, all_data): + try: + content = field_data['content'] + except TypeError: + raise validators.CriticalValidationError, gettext("No file was submitted. Check the encoding type on the form.") + if not content: + raise validators.CriticalValidationError, gettext("The submitted file is empty.") + + def render(self, data): + return '' % \ + (self.get_id(), self.__class__.__name__, self.field_name) + + def html2python(data): + if data is None: + raise EmptyValue + return data + html2python = staticmethod(html2python) + +class ImageUploadField(FileUploadField): + "A FileUploadField that raises CriticalValidationError if the uploaded file isn't an image." + def __init__(self, *args, **kwargs): + FileUploadField.__init__(self, *args, **kwargs) + self.validator_list.insert(0, self.isValidImage) + + def isValidImage(self, field_data, all_data): + try: + validators.isValidImage(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +#################### +# INTEGERS/FLOATS # +#################### + +class IntegerField(TextField): + def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + validator_list = [self.isInteger] + validator_list + if member_name is not None: + self.member_name = member_name + TextField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isInteger(self, field_data, all_data): + try: + validators.isInteger(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + if data == '' or data is None: + return None + return int(data) + html2python = staticmethod(html2python) + +class SmallIntegerField(IntegerField): + def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isSmallInteger] + validator_list + IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isSmallInteger(self, field_data, all_data): + if not -32768 <= int(field_data) <= 32767: + raise validators.CriticalValidationError, gettext("Enter a whole number between -32,768 and 32,767.") + +class PositiveIntegerField(IntegerField): + def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isPositive] + validator_list + IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isPositive(self, field_data, all_data): + if int(field_data) < 0: + raise validators.CriticalValidationError, gettext("Enter a positive number.") + +class PositiveSmallIntegerField(IntegerField): + def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isPositiveSmall] + validator_list + IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isPositiveSmall(self, field_data, all_data): + if not 0 <= int(field_data) <= 32767: + raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.") + +class FloatField(TextField): + def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.max_digits, self.decimal_places = max_digits, decimal_places + validator_list = [self.isValidFloat] + validator_list + TextField.__init__(self, field_name, max_digits+2, max_digits+2, is_required, validator_list) + + def isValidFloat(self, field_data, all_data): + v = validators.IsValidFloat(self.max_digits, self.decimal_places) + try: + v(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + if data == '' or data is None: + return None + return float(data) + html2python = staticmethod(html2python) + +#################### +# DATES AND TIMES # +#################### + +class DatetimeField(TextField): + """A FormField that automatically converts its data to a datetime.datetime object. + The data should be in the format YYYY-MM-DD HH:MM:SS.""" + def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.length, self.maxlength = length, maxlength + self.is_required = is_required + self.validator_list = [validators.isValidANSIDatetime] + validator_list + + def html2python(data): + "Converts the field into a datetime.datetime object" + import datetime + try: + date, time = data.split() + y, m, d = date.split('-') + timebits = time.split(':') + h, mn = timebits[:2] + if len(timebits) > 2: + s = int(timebits[2]) + else: + s = 0 + return datetime.datetime(int(y), int(m), int(d), int(h), int(mn), s) + except ValueError: + return None + html2python = staticmethod(html2python) + +class DateField(TextField): + """A FormField that automatically converts its data to a datetime.date object. + The data should be in the format YYYY-MM-DD.""" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidDate] + validator_list + TextField.__init__(self, field_name, length=10, maxlength=10, + is_required=is_required, validator_list=validator_list) + + def isValidDate(self, field_data, all_data): + try: + validators.isValidANSIDate(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + "Converts the field into a datetime.date object" + import time, datetime + try: + time_tuple = time.strptime(data, '%Y-%m-%d') + return datetime.date(*time_tuple[0:3]) + except (ValueError, TypeError): + return None + html2python = staticmethod(html2python) + +class TimeField(TextField): + """A FormField that automatically converts its data to a datetime.time object. + The data should be in the format HH:MM:SS or HH:MM:SS.mmmmmm.""" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidTime] + validator_list + TextField.__init__(self, field_name, length=8, maxlength=8, + is_required=is_required, validator_list=validator_list) + + def isValidTime(self, field_data, all_data): + try: + validators.isValidANSITime(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + "Converts the field into a datetime.time object" + import time, datetime + try: + part_list = data.split('.') + try: + time_tuple = time.strptime(part_list[0], '%H:%M:%S') + except ValueError: # seconds weren't provided + time_tuple = time.strptime(part_list[0], '%H:%M') + t = datetime.time(*time_tuple[3:6]) + if (len(part_list) == 2): + t = t.replace(microsecond=int(part_list[1])) + return t + except (ValueError, TypeError, AttributeError): + return None + html2python = staticmethod(html2python) + +#################### +# INTERNET-RELATED # +#################### + +class EmailField(TextField): + "A convenience FormField for validating e-mail addresses" + def __init__(self, field_name, length=50, maxlength=75, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidEmail] + validator_list + TextField.__init__(self, field_name, length, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isValidEmail(self, field_data, all_data): + try: + validators.isValidEmail(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +class URLField(TextField): + "A convenience FormField for validating URLs" + def __init__(self, field_name, length=50, maxlength=200, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidURL] + validator_list + TextField.__init__(self, field_name, length=length, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isValidURL(self, field_data, all_data): + try: + validators.isValidURL(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +class IPAddressField(TextField): + def __init__(self, field_name, length=15, maxlength=15, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidIPAddress] + validator_list + TextField.__init__(self, field_name, length=length, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isValidIPAddress(self, field_data, all_data): + try: + validators.isValidIPAddress4(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + return data or None + html2python = staticmethod(html2python) + +#################### +# MISCELLANEOUS # +#################### + +class FilePathField(SelectField): + "A SelectField whose choices are the files in a given directory." + def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None): + import os + from django.db.models import BLANK_CHOICE_DASH + if match is not None: + import re + match_re = re.compile(match) + choices = not is_required and BLANK_CHOICE_DASH[:] or [] + if recursive: + for root, dirs, files in os.walk(path): + for f in files: + if match is None or match_re.search(f): + choices.append((os.path.join(root, f), f)) + else: + try: + for f in os.listdir(path): + full_file = os.path.join(path, f) + if os.path.isfile(full_file) and (match is None or match_re.search(f)): + choices.append((full_file, f)) + except OSError: + pass + SelectField.__init__(self, field_name, choices, 1, is_required, validator_list) + +class PhoneNumberField(TextField): + "A convenience FormField for validating phone numbers (e.g. '630-555-1234')" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidPhone] + validator_list + TextField.__init__(self, field_name, length=12, maxlength=12, + is_required=is_required, validator_list=validator_list) + + def isValidPhone(self, field_data, all_data): + try: + validators.isValidPhone(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +class USStateField(TextField): + "A convenience FormField for validating U.S. states (e.g. 'IL')" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidUSState] + validator_list + TextField.__init__(self, field_name, length=2, maxlength=2, + is_required=is_required, validator_list=validator_list) + + def isValidUSState(self, field_data, all_data): + try: + validators.isValidUSState(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + return data.upper() # Should always be stored in upper case + html2python = staticmethod(html2python) + +class CommaSeparatedIntegerField(TextField): + "A convenience FormField for validating comma-separated integer fields" + def __init__(self, field_name, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isCommaSeparatedIntegerList] + validator_list + TextField.__init__(self, field_name, length=20, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isCommaSeparatedIntegerList(self, field_data, all_data): + try: + validators.isCommaSeparatedIntegerList(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def render(self, data): + if data is None: + data = '' + elif isinstance(data, (list, tuple)): + data = ','.join(data) + return super(CommaSeparatedIntegerField, self).render(data) + +class RawIdAdminField(CommaSeparatedIntegerField): + def html2python(data): + if data: + return data.split(',') + else: + return [] + html2python = staticmethod(html2python) + +class XMLLargeTextField(LargeTextField): + """ + A LargeTextField with an XML validator. The schema_path argument is the + full path to a Relax NG compact schema to validate against. + """ + def __init__(self, field_name, schema_path, **kwargs): + self.schema_path = schema_path + kwargs.setdefault('validator_list', []).insert(0, self.isValidXML) + LargeTextField.__init__(self, field_name, **kwargs) + + def isValidXML(self, field_data, all_data): + v = validators.RelaxNGCompact(self.schema_path) + try: + v(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages 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/django/utils/dateformat.py b/django/utils/dateformat.py index 0890a81a81..00eb9fe617 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -11,7 +11,7 @@ Usage: >>> """ -from django.utils.dates import MONTHS, MONTHS_AP, WEEKDAYS +from django.utils.dates import MONTHS, MONTHS_3, MONTHS_AP, WEEKDAYS from django.utils.tzinfo import LocalTimezone from calendar import isleap, monthrange import re, time @@ -147,7 +147,7 @@ class DateFormat(TimeFormat): def M(self): "Month, textual, 3 letters; e.g. 'Jan'" - return MONTHS[self.data.month][0:3] + return MONTHS_3[self.data.month].title() def n(self): "Month without leading zeros; i.e. '1' to '12'" diff --git a/django/utils/text.py b/django/utils/text.py index 9e7bb3b6c4..217f42491b 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -8,17 +8,28 @@ capfirst = lambda x: x and x[0].upper() + x[1:] def wrap(text, width): """ A word-wrap function that preserves existing line breaks and most spaces in - the text. Expects that existing line breaks are posix newlines (\n). - See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061 + the text. Expects that existing line breaks are posix newlines. """ - return reduce(lambda line, word, width=width: '%s%s%s' % - (line, - ' \n'[(len(line[line.rfind('\n')+1:]) - + len(word.split('\n',1)[0] - ) >= width)], - word), - text.split(' ') - ) + def _generator(): + it = iter(text.split(' ')) + word = it.next() + yield word + pos = len(word) - word.rfind('\n') - 1 + for word in it: + if "\n" in word: + lines = word.splitlines() + else: + lines = (word,) + pos += len(lines[0]) + 1 + if pos > width: + yield '\n' + pos = len(lines[-1]) + else: + yield ' ' + if len(lines) > 1: + pos = len(lines[-1]) + yield word + return "".join(_generator()) def truncate_words(s, num): "Truncates a string after a certain number of words." diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py index 3a03fa59e4..28987f7544 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -1,6 +1,6 @@ from django.core.xheaders import populate_xheaders from django.template import loader -from django import forms +from django import oldforms from django.db.models import FileField from django.contrib.auth.views import redirect_to_login from django.template import RequestContext @@ -56,7 +56,7 @@ def create_object(request, model, template_name=None, new_data = manipulator.flatten_data() # Create the FormWrapper, template, context, response - form = forms.FormWrapper(manipulator, new_data, errors) + form = oldforms.FormWrapper(manipulator, new_data, errors) if not template_name: template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) @@ -128,7 +128,7 @@ def update_object(request, model, object_id=None, slug=None, # This makes sure the form acurate represents the fields of the place. new_data = manipulator.flatten_data() - form = forms.FormWrapper(manipulator, new_data, errors) + form = oldforms.FormWrapper(manipulator, new_data, errors) if not template_name: template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index bd0f17c56a..1836ce4a9f 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -84,7 +84,7 @@ def object_detail(request, queryset, object_id=None, slug=None, context_processors=None, template_object_name='object', mimetype=None): """ - Generic list of objects. + Generic detail of an object. Templates: ``/_detail.html`` Context: 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/contributing.txt b/docs/contributing.txt index 6ff0b038a3..de9236f6c1 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -22,6 +22,9 @@ of the community, so there are many ways you can help Django's development: likely to be skeptical of large-scale suggestions without some code to back it up. + * Triage patches that have been submitted by other users. Please read + `Ticket triage`_ below, for details on the triage process. + That's all you need to know if you'd like to join the Django development community. The rest of this document describes the details of how our community works and how it handles bugs, mailing lists, and all the other minutiae of @@ -44,8 +47,10 @@ particular: * **Do** write complete, reproducible, specific bug reports. Include as much information as you possibly can, complete with code snippets, test - cases, etc. A minimal example that illustrates the bug in a nice small - test case is the best possible bug report. + cases, etc. This means including a clear, concise description of the + problem, and a clear set of instructions for replicating the problem. + A minimal example that illustrates the bug in a nice small test case + is the best possible bug report. * **Don't** use the ticket system to ask support questions. Use the `django-users`_ list, or the `#django`_ IRC channel for that. @@ -121,6 +126,50 @@ Patch style it obvious that the ticket includes a patch, and it will add the ticket to the `list of tickets with patches`_. + * The code required to fix a problem or add a feature is an essential part + of a patch, but it is not the only part. A good patch should also include + a regression test to validate the behavior that has been fixed (and prevent + the problem from arising again). + + * If the code associated with a patch adds a new feature, or modifies behavior + of an existing feature, the patch should also contain documentation. + +Non-trivial patches +------------------- + +A "non-trivial" patch is one that is more than a simple bug fix. It's a patch +that introduces Django functionality and makes some sort of design decision. + +If you provide a non-trivial patch, include evidence that alternatives have +been discussed on `django-developers`_. If you're not sure whether your patch +should be considered non-trivial, just ask. + +Ticket triage +============= + +Unfortunately, not all bug reports in the `ticket tracker`_ provide all +the `required details`_. A number of tickets have patches, but those patches +don't meet all the requirements of a `good patch`_. + +One way to help out is to *triage* bugs that have been reported by other users. +Pick an open ticket that is missing some details, and try to replicate the +problem. Fill in the missing pieces of the report. If the ticket doesn't have +a patch, create one. + +Once you've completed all the missing details on the ticket and you have a +patch with all the required features, e-mail `django-developers`_. Indicate +that you have triaged a ticket, and recommend a course of action for dealing +with that ticket. + +At first, this may require you to be persistent. If you find that your triaged +ticket still isn't getting attention, occasional polite requests for eyeballs +to look at your ticket may be necessary. However, as you earn a reputation for +quality triage work, you should find that it is easier to get the developers' +attention. + +.. _required details: `Reporting bugs`_ +.. _good patch: `Patch style`_ + Submitting and maintaining translations ======================================= @@ -338,21 +387,63 @@ trunk more than once. Using branches -------------- -To test a given branch, you can simply check out the entire branch, like so:: +To use a branch, you'll need to do two things: + + * Get the branch's code through Subversion. + + * Point your Python ``site-packages`` directory at the branch's version of + the ``django`` package rather than the version you already have + installed. + +Getting the code from Subversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the latest version of a branch's code, check it out using Subversion:: svn co http://code.djangoproject.com/svn/django/branches// -Or, if you've got a working directory you'd like to switch to use a branch, -you can use:: +...where ```` is the branch's name. See the `list of branch names`_. + +Alternatively, you can automatically convert an existing directory of the +Django source code as long as you've checked it out via Subversion. To do the +conversion, execute this command from within your ``django`` directory:: svn switch http://code.djangoproject.com/svn/django/branches// -...in the root of your Django sandbox (the directory that contains ``django``, -``docs``, and ``tests``). - The advantage of using ``svn switch`` instead of ``svn co`` is that the ``switch`` command retains any changes you might have made to your local copy -of the code. It attempts to merge those changes into the "switched" code. +of the code. It attempts to merge those changes into the "switched" code. The +disadvantage is that it may cause conflicts with your local changes if the +"switched" code has altered the same lines of code. + +(Note that if you use ``svn switch``, you don't need to point Python at the new +version, as explained in the next section.) + +.. _list of branch names: http://code.djangoproject.com/browser/django/branches + +Pointing Python at the new Django version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you've retrieved the branch's code, you'll need to change your Python +``site-packages`` directory so that it points to the branch version of the +``django`` directory. (The ``site-packages`` directory is somewhere such as +``/usr/lib/python2.4/site-packages`` or +``/usr/local/lib/python2.4/site-packages`` or ``C:\Python\site-packages``.) + +The simplest way to do this is by renaming the old ``django`` directory to +``django.OLD`` and moving the trunk version of the code into the directory +and calling it ``django``. + +Alternatively, you can use a symlink called ``django`` that points to the +location of the branch's ``django`` package. If you want to switch back, just +change the symlink to point to the old code. + +If you're using Django 0.95 or earlier and installed it using +``python setup.py install``, you'll have a directory called something like +``Django-0.95-py2.4.egg`` instead of ``django``. In this case, edit the file +``setuptools.pth`` and remove the line that references the Django ``.egg`` +file. Then copy the branch's version of the ``django`` directory into +``site-packages``. Official releases ================= diff --git a/docs/csrf.txt b/docs/csrf.txt index 218b43a61a..c12dd1d116 100644 --- a/docs/csrf.txt +++ b/docs/csrf.txt @@ -1,9 +1,9 @@ ===================================== -Cross Site Request Forgery Protection +Cross Site Request Forgery protection ===================================== -The CsrfMiddleware class provides easy-to-use protection against -`Cross Site Request Forgeries`_. This type of attack occurs when a malicious +The CsrfMiddleware class provides easy-to-use protection against +`Cross Site Request Forgeries`_. This type of attack occurs when a malicious web site creates a link or form button that is intended to perform some action on your web site, using the credentials of a logged-in user who is tricked into clicking on the link in their browser. @@ -12,12 +12,12 @@ The first defense against CSRF attacks is to ensure that GET requests are side-effect free. POST requests can then be protected by adding this middleware into your list of installed middleware. - .. _Cross Site Request Forgeries: http://www.squarefree.com/securitytips/web-developers.html#CSRF How to use it ============= -Add the middleware ``'django.contrib.csrf.middleware.CsrfMiddleware'`` to + +Add the middleware ``'django.contrib.csrf.middleware.CsrfMiddleware'`` to your list of middleware classes, ``MIDDLEWARE_CLASSES``. It needs to process the response after the SessionMiddleware, so must come before it in the list. It also must process the response before things like compression @@ -25,16 +25,17 @@ happen to the response, so it must come after GZipMiddleware in the list. How it works ============ + CsrfMiddleware does two things: -1. It modifies outgoing requests by adding a hidden form field to all - 'POST' forms, with the name 'csrfmiddlewaretoken' and a value which is - a hash of the session ID plus a secret. If there is no session ID set, - this modification of the response isn't done, so there is very little +1. It modifies outgoing requests by adding a hidden form field to all + 'POST' forms, with the name 'csrfmiddlewaretoken' and a value which is + a hash of the session ID plus a secret. If there is no session ID set, + this modification of the response isn't done, so there is very little performance penalty for those requests that don't have a session. -2. On all incoming POST requests that have the session cookie set, it - checks that the 'csrfmiddlewaretoken' is present and correct. If it +2. On all incoming POST requests that have the session cookie set, it + checks that the 'csrfmiddlewaretoken' is present and correct. If it isn't, the user will get a 403 error. This ensures that only forms that have originated from your web site @@ -43,26 +44,26 @@ can be used to POST data back. It deliberately only targets HTTP POST requests (and the corresponding POST forms). GET requests ought never to have side effects (if you are using HTTP GET and POST correctly), and so a CSRF attack with a GET -request will always be harmless. +request will always be harmless. POST requests that are not accompanied by a session cookie are not protected, but they do not need to be protected, since the 'attacking' web site could make these kind of requests anyway. -The Content-Type is checked before modifying the response, and only +The Content-Type is checked before modifying the response, and only pages that are served as 'text/html' or 'application/xml+xhtml' are modified. Limitations =========== + CsrfMiddleware requires Django's session framework to work. If you have a custom authentication system that manually sets cookies and the like, it won't help you. -If your app creates HTML pages and forms in some unusual way, (e.g. -it sends fragments of HTML in javascript document.write statements) -you might bypass the filter that adds the hidden field to the form, +If your app creates HTML pages and forms in some unusual way, (e.g. +it sends fragments of HTML in javascript document.write statements) +you might bypass the filter that adds the hidden field to the form, in which case form submission will always fail. It may still be possible to use the middleware, provided you can find some way to get the -CSRF token and ensure that is included when your form is submitted. - +CSRF token and ensure that is included when your form is submitted. \ No newline at end of file diff --git a/docs/db-api.txt b/docs/db-api.txt index 2f0c8b0589..13a32bd0b8 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -143,9 +143,9 @@ or ``UPDATE`` SQL statements. Specifically, when you call ``save()``, Django follows this algorithm: * If the object's primary key attribute is set to a value that evaluates to - ``False`` (such as ``None`` or the empty string), Django executes a - ``SELECT`` query to determine whether a record with the given primary key - already exists. + ``True`` (i.e., a value other than ``None`` or the empty string), Django + executes a ``SELECT`` query to determine whether a record with the given + primary key already exists. * If the record with the given primary key does already exist, Django executes an ``UPDATE`` query. * If the object's primary key attribute is *not* set, or if it's set but a diff --git a/docs/forms.txt b/docs/forms.txt index ff192a4717..6efa24d28f 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -2,15 +2,27 @@ Forms, fields, and manipulators =============================== +Forwards-compatibility note +=========================== + +The legacy forms/manipulators system described in this document is going to be +replaced in the next Django release. If you're starting from scratch, we +strongly encourage you not to waste your time learning this. Instead, learn and +use the django.newforms system, which we have begun to document in the +`newforms documentation`_. + +If you have legacy form/manipulator code, read the "Migration plan" section in +that document to understand how we're making the switch. + +.. _newforms documentation: http://www.djangoproject.com/documentation/newforms/ + +Introduction +============ + Once you've got a chance to play with Django's admin interface, you'll probably wonder if the fantastic form validation framework it uses is available to user code. It is, and this document explains how the framework works. - .. admonition:: A note to the lazy - - If all you want to do is present forms for a user to create and/or - update a given object, you may be able to use `generic views`_. - We'll take a top-down approach to examining Django's form validation framework, because much of the time you won't need to use the lower-level APIs. Throughout this document, we'll be working with the following model, a "place" object:: @@ -41,17 +53,17 @@ this document, we'll be working with the following model, a "place" object:: Defining the above class is enough to create an admin interface to a ``Place``, but what if you want to allow public users to submit places? -Manipulators -============ +Automatic Manipulators +====================== The highest-level interface for object creation and modification is the -**Manipulator** framework. A manipulator is a utility class tied to a given -model that "knows" how to create or modify instances of that model and how to -validate data for the object. Manipulators come in two flavors: -``AddManipulators`` and ``ChangeManipulators``. Functionally they are quite -similar, but the former knows how to create new instances of the model, while -the latter modifies existing instances. Both types of classes are automatically -created when you define a new class:: +**automatic Manipulator** framework. An automatic manipulator is a utility +class tied to a given model that "knows" how to create or modify instances of +that model and how to validate data for the object. Automatic Manipulators come +in two flavors: ``AddManipulators`` and ``ChangeManipulators``. Functionally +they are quite similar, but the former knows how to create new instances of the +model, while the latter modifies existing instances. Both types of classes are +automatically created when you define a new class:: >>> from mysite.myapp.models import Place >>> Place.AddManipulator diff --git a/docs/generic_views.txt b/docs/generic_views.txt index 1736770a20..25635f35d8 100644 --- a/docs/generic_views.txt +++ b/docs/generic_views.txt @@ -902,7 +902,7 @@ If ``template_name`` isn't specified, this view will use the template In addition to ``extra_context``, the template's context will be: - * ``form``: A ``django.forms.FormWrapper`` instance representing the form + * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form for editing the object. This lets you refer to form fields easily in the template system. @@ -984,7 +984,7 @@ If ``template_name`` isn't specified, this view will use the template In addition to ``extra_context``, the template's context will be: - * ``form``: A ``django.forms.FormWrapper`` instance representing the form + * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form for editing the object. This lets you refer to form fields easily in the template system. diff --git a/docs/newforms.txt b/docs/newforms.txt new file mode 100644 index 0000000000..c4986eb45e --- /dev/null +++ b/docs/newforms.txt @@ -0,0 +1,300 @@ +==================== +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: + + * As of revision [4208], we've copied the current ``django.forms`` to + ``django.oldforms``. This allows you to upgrade your code *now* rather + than waiting for the backwards-incompatible change and rushing to fix + your code after the fact. Just change your import statements like this:: + + from django import forms # old + from django import oldforms as forms # new + + * At an undecided future date, we will move the current ``django.newforms`` + to ``django.forms``. This will be a backwards-incompatible change, and + anybody who is still using the old version of ``django.forms`` at that + time will need to change their import statements, as described in the + previous bullet. + + * 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 the new ``django.forms``. + +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`` ("manipulators") 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. + +For example, if your Web site has a contact form that visitors can use to +send you e-mail, you'd use this library to implement the display of the HTML +form fields, along with the form validation. Any time you need to use an HTML +``
    ``, you can use this library. + +The library deals with these concepts: + + * **Widget** -- A class that corresponds to an HTML form widget, e.g. + ```` or `` -as_textarea() and as_text() are shortcuts for changing the output widget type: +as_textarea(), as_text() and as_hidden() are shortcuts for changing the output +widget type: >>> f['subject'].as_textarea() u'' >>> f['message'].as_text() u'' +>>> f['message'].as_hidden() +u'' The 'widget' parameter to a Field can also be an instance: >>> class ContactForm(Form): ... subject = CharField() ... message = CharField(widget=Textarea(attrs={'rows': 80, 'cols': 20})) ->>> f = ContactForm() +>>> f = ContactForm(auto_id=False) >>> print f['message'] -Instance-level attrs are *not* carried over to as_textarea() and as_text(): +Instance-level attrs are *not* carried over to as_textarea(), as_text() and +as_hidden(): >>> f['message'].as_text() u'' ->>> f = ContactForm({'subject': 'Hello', 'message': 'I love you.'}) +>>> f = ContactForm({'subject': 'Hello', 'message': 'I love you.'}, auto_id=False) >>> f['subject'].as_textarea() u'' >>> f['message'].as_text() u'' +>>> f['message'].as_hidden() +u'' For a form with a ->>> f = FrameworkForm({'name': 'Django', 'language': 'P'}) +>>> f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False) >>> print f['language'] +Add widget=RadioSelect to use that widget with a ChoiceField. +>>> class FrameworkForm(Form): +... name = CharField() +... language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')], widget=RadioSelect) +>>> f = FrameworkForm(auto_id=False) +>>> print f['language'] +
      +
    • +
    • +
    +>>> print f +Name: +Language:
      +
    • +
    • +
    +>>> print f.as_ul() +
  • Name:
  • +
  • Language:
      +
    • +
    • +
  • + +Regarding auto_id and