diff options
| author | Adrian Holovaty <adrian@holovaty.com> | 2006-12-27 02:49:28 +0000 |
|---|---|---|
| committer | Adrian Holovaty <adrian@holovaty.com> | 2006-12-27 02:49:28 +0000 |
| commit | 0cf7bc439129c66df8d64601e885f83b256b4f25 (patch) | |
| tree | a7fd3cd4df5e862578544778d1b720ded59b7274 /django | |
| parent | c4673e4fb68237f96652d7372bec0283c5446c27 (diff) | |
per-object-permissions: Merged to trunk [4241]
git-svn-id: http://code.djangoproject.com/svn/django/branches/per-object-permissions@4242 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
44 files changed, 1937 insertions, 1318 deletions
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 @@ <div id="content" class="{% block coltype %}colM{% endblock %}"> {% block pretitle %}{% endblock %} {% block content_title %}{% if title %}<h1>{{ title|escape }}</h1>{% endif %}{% endblock %} - {% block content %}{{ content }}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} {% block sidebar %}{% endblock %} <br class="clear" /> </div> 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 @@ </div> {% endif %}{% endblock %} {% block content %}<div id="content-main"> +{% block object-tools %} {% if change %}{% if not is_popup %} <ul class="object-tools"><li><a href="history/" class="historylink">{% trans "History" %}</a></li> {% if object_has_row_level_permissions and has_row_level_permissions %}<li><a href="row_level_permissions/" class="rowlevelpermissions">{% trans "Edit Row Level Permissions" %}</a></li>{% endif %} {% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%} </ul> {% endif %}{% endif %} +{% endblock %} <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %} <div> {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% 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 %} <div id="content-main"> +{% block object-tools %} {% if has_add_permission %} <ul class="object-tools"><li><a href="add/{% if is_popup %}?_popup=1{% endif %}" class="addlink">{% blocktrans with cl.opts.verbose_name|escape as name %}Add {{ name }}{% endblocktrans %}</a></li></ul> {% endif %} +{% endblock %} <div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist"> {% 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 @@ <input type="text" size="40" name="{{ search_var }}" value="{{ cl.query|escape }}" id="searchbar" /> <input type="submit" value="{% trans 'Go' %}" /> {% if show_result_count %} - <span class="small quiet">{% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?">{% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}</a>)</span> + <span class="small quiet">{% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?{% if cl.is_popup %}pop=1{% endif %}">{% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}</a>)</span> {% endif %} {% for pair in cl.params.items %} {% ifnotequal pair.0 search_var %}<input type="hidden" name="{{ pair.0|escape }}" value="{{ pair.1|escape }}"/>{% endifnotequal %} diff --git a/django/contrib/admin/views/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 = "<h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p>" +_ERROR_MSG = '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><body><h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p></body></html>' _POST_FORM_RE = \ re.compile(r'(<form\W[^>]*\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 --- /dev/null +++ b/django/contrib/formtools/__init__.py diff --git a/django/contrib/formtools/preview.py b/django/contrib/formtools/preview.py new file mode 100644 index 0000000000..9a9371b5f8 --- /dev/null +++ b/django/contrib/formtools/preview.py @@ -0,0 +1,160 @@ +""" +Formtools Preview application. + +This is an abstraction of the following workflow: + + "Display an HTML form, force a preview, then do something with the submission." + +Given a django.newforms.Form object that you define, this takes care of the +following: + + * Displays the form as HTML on a Web page. + * Validates the form data once it's submitted via POST. + * If it's valid, displays a preview page. + * If it's not valid, redisplays the form with error messages. + * At the preview page, if the preview confirmation button is pressed, calls + a hook that you define -- a done() method. + +The framework enforces the required preview by passing a shared-secret hash to +the preview page. If somebody tweaks the form parameters on the preview page, +the form submission will fail the hash comparison test. + +Usage +===== + +Subclass FormPreview and define a done() method: + + def done(self, request, clean_data): + # ... + +This method takes an HttpRequest object and a dictionary of the form data after +it has been validated and cleaned. It should return an HttpResponseRedirect. + +Then, just instantiate your FormPreview subclass by passing it a Form class, +and pass that to your URLconf, like so: + + (r'^post/$', MyFormPreview(MyForm)), + +The FormPreview class has a few other hooks. See the docstrings in the source +code below. + +The framework also uses two templates: 'formtools/preview.html' and +'formtools/form.html'. You can override these by setting 'preview_template' and +'form_template' attributes on your FormPreview subclass. See +django/contrib/formtools/templates for the default templates. +""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404 +from django.shortcuts import render_to_response +import cPickle as pickle +import md5 + +AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. + +class FormPreview(object): + preview_template = 'formtools/preview.html' + form_template = 'formtools/form.html' + + # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### + + def __init__(self, form): + # form should be a Form class, not an instance. + self.form, self.state = form, {} + + def __call__(self, request, *args, **kwargs): + stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview') + self.parse_params(*args, **kwargs) + try: + method = getattr(self, stage + '_' + request.method.lower()) + except AttributeError: + raise Http404 + return method(request) + + def unused_name(self, name): + """ + Given a first-choice name, adds an underscore to the name until it + reaches a name that isn't claimed by any field in the form. + + This is calculated rather than being hard-coded so that no field names + are off-limits for use in the form. + """ + while 1: + try: + f = self.form.fields[name] + except KeyError: + break # This field name isn't being used by the form. + name += '_' + return name + + def preview_get(self, request): + "Displays the form" + f = self.form(auto_id=AUTO_ID) + return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}) + + def preview_post(self, request): + "Validates the POST data. If valid, displays the preview page. Else, redisplays form." + f = self.form(request.POST, auto_id=AUTO_ID) + context = {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state} + if f.is_valid(): + context['hash_field'] = self.unused_name('hash') + context['hash_value'] = self.security_hash(request, f) + return render_to_response(self.preview_template, context) + else: + return render_to_response(self.form_template, context) + + def post_post(self, request): + "Validates the POST data. If valid, calls done(). Else, redisplays form." + f = self.form(request.POST, auto_id=AUTO_ID) + if f.is_valid(): + if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')): + return self.failed_hash(request) # Security hash failed. + return self.done(request, f.clean_data) + else: + return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}) + + # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## + + def parse_params(self, *args, **kwargs): + """ + Given captured args and kwargs from the URLconf, saves something in + self.state and/or raises Http404 if necessary. + + For example, this URLconf captures a user_id variable: + + (r'^contact/(?P<user_id>\d{1,6})/$', MyFormPreview(MyForm)), + + In this case, the kwargs variable in parse_params would be + {'user_id': 32} for a request to '/contact/32/'. You can use that + user_id to make sure it's a valid user and/or save it for later, for + use in done(). + """ + pass + + def security_hash(self, request, form): + """ + Calculates the security hash for the given Form instance. + + This creates a list of the form field names/values in a deterministic + order, pickles the result with the SECRET_KEY setting and takes an md5 + hash of that. + + Subclasses may want to take into account request-specific information + such as the IP address. + """ + data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY] + # Use HIGHEST_PROTOCOL because it's the most efficient. It requires + # Python 2.3, but Django requires 2.3 anyway, so that's OK. + pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL) + return md5.new(pickled).hexdigest() + + def failed_hash(self, request): + "Returns an HttpResponse in the case of an invalid security hash." + return self.preview_post(request) + + # METHODS SUBCLASSES MUST OVERRIDE ######################################## + + def done(self, request, clean_data): + "Does something with the clean_data and returns an HttpResponseRedirect." + raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__) diff --git a/django/contrib/formtools/templates/formtools/form.html b/django/contrib/formtools/templates/formtools/form.html new file mode 100644 index 0000000000..90da8b2b2b --- /dev/null +++ b/django/contrib/formtools/templates/formtools/form.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block content %} + +{% if form.errors %}<h1>Please correct the following errors</h1>{% else %}<h1>Submit</h1>{% endif %} + +<form action="" method="post"> +<table> +{{ form }} +</table> +<input type="hidden" name="{{ stage_field }}" value="1" /> +<p><input type="submit" value="Submit" /></p> +</form> + +{% endblock %} diff --git a/django/contrib/formtools/templates/formtools/preview.html b/django/contrib/formtools/templates/formtools/preview.html new file mode 100644 index 0000000000..c7955d46e1 --- /dev/null +++ b/django/contrib/formtools/templates/formtools/preview.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} + +<h1>Preview your submission</h1> + +<table> +{% for field in form %} +<tr> +<th>{{ field.verbose_name }}:</th> +<td>{{ field.data|escape }}</td> +</tr> +{% endfor %} +</table> + +<p>Security hash: {{ hash_value }}</p> + +<form action="" method="post"> +{% for field in form %}{{ field.as_hidden }} +{% endfor %} +<input type="hidden" name="{{ stage_field }}" value="2" /> +<input type="hidden" name="{{ hash_field }}" value="{{ hash_value }}" /> +<p><input type="submit" value="Submit" /></p> +</form> + +<h1>Or edit it again</h1> + +<form action="" method="post"> +<table> +{{ form }} +</table> +<input type="hidden" name="{{ stage_field }}" value="1" /> +<p><input type="submit" value="Submit changes" /></p> +</form> + +{% endblock %} diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 2c76e13c22..44ede4460a 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -29,7 +29,7 @@ def ping_google(sitemap_url=None, ping_url=PING_URL): from django.contrib.sites.models import Site current_site = Site.objects.get_current() - url = "%s%s" % (current_site.domain, sitemap) + url = "%s%s" % (current_site.domain, sitemap_url) params = urllib.urlencode({'sitemap':url}) urllib.urlopen("%s?%s" % (ping_url, params)) diff --git a/django/core/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 '<FormFieldWrapper for "%s">' % 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 '<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.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 "<FormFieldCollection: %s>" % 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 '<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \ - (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 '<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \ - (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 '<input type="hidden" id="%s" name="%s" value="%s" />' % \ - (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 '<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \ - (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 = ['<select id="%s" class="v%s%s" name="%s" size="%s">' % \ - (self.get_id(), self.__class__.__name__, - self.is_required and ' required' or '', self.field_name, self.size)] - str_data = str(data) # normalize to string - for value, display_name in self.choices: - selected_html = '' - if str(value) == str_data: - selected_html = ' selected="selected"' - output.append(' <option value="%s"%s>%s</option>' % (escape(value), selected_html, escape(display_name))) - output.append(' </select>') - 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 }}<br /> - {% 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 <ul>" - output = ['<ul%s>' % (self.ul_class and ' class="%s"' % self.ul_class or '')] - output.extend(['<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist]) - output.append('</ul>') - 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': '<input type="radio" id="%s" name="%s" value="%s"%s/>' % \ - (self.get_id() + '_' + str(i), self.field_name, value, selected_html), - 'label': '<label for="%s">%s</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 = ['<select id="%s" class="v%s%s" name="%s" size="%s" multiple="multiple">' % \ - (self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', - self.field_name, self.size)] - str_data_list = map(str, data) # normalize to strings - for value, choice in self.choices: - selected_html = '' - if str(value) in str_data_list: - selected_html = ' selected="selected"' - output.append(' <option value="%s"%s>%s</option>' % (escape(value), selected_html, escape(choice))) - output.append(' </select>') - 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 <select multiple>, this widget outputs a - <ul> of <input type="checkbox">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 = ['<ul%s>' % (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('<li><input type="checkbox" id="%s" class="v%s" name="%s"%s value="on" /> <label for="%s">%s</label></li>' % \ - (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, - self.get_id() + escape(value), choice)) - output.append('</ul>') - 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 '<input type="file" id="%s" class="v%s" name="%s" />' % \ - (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 <select> boxes. + + This also serves as an example of a Widget that has more than one HTML + element and hence implements value_from_datadict. + """ + month_field = '%s_month' + day_field = '%s_day' + year_field = '%s_year' + + def __init__(self, attrs=None, years=None): + # years is an optional list/tuple of years to use in the "year" select box. + self.attrs = attrs or {} + if years: + self.years = years + else: + this_year = datetime.date.today().year + self.years = range(this_year, this_year+10) + + def render(self, name, value, attrs=None): + try: + value = datetime.date(*map(int, value.split('-'))) + year_val, month_val, day_val = value.year, value.month, value.day + except (AttributeError, TypeError, ValueError): + year_val = month_val = day_val = None + + output = [] + + month_choices = MONTHS.items() + month_choices.sort() + select_html = Select(choices=month_choices).render(self.month_field % name, month_val) + output.append(select_html) + + day_choices = [(i, i) for i in range(1, 32)] + select_html = Select(choices=day_choices).render(self.day_field % name, day_val) + output.append(select_html) + + year_choices = [(i, i) for i in self.years] + select_html = Select(choices=year_choices).render(self.year_field % name, year_val) + output.append(select_html) + + return u'\n'.join(output) + + def value_from_datadict(self, data, name): + y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name) + if y and m and d: + return '%s-%s-%s' % (y, m, d) + return None diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 40fc18bd3e..5c6d46ddac 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -2,8 +2,9 @@ Field classes """ -from util import ValidationError, DEFAULT_ENCODING, smart_unicode -from widgets import TextInput, CheckboxInput, Select, SelectMultiple +from django.utils.translation import gettext +from util import ValidationError, smart_unicode +from widgets import TextInput, PasswordInput, CheckboxInput, Select, SelectMultiple import datetime import re import time @@ -11,6 +12,7 @@ import time __all__ = ( 'Field', 'CharField', 'IntegerField', 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', + 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'RegexField', 'EmailField', 'URLField', 'BooleanField', 'ChoiceField', 'MultipleChoiceField', @@ -31,11 +33,19 @@ class Field(object): # Tracks each time a Field instance is created. Used to retain order. creation_counter = 0 - def __init__(self, required=True, widget=None): - self.required = required + def __init__(self, required=True, widget=None, label=None): + if label is not None: + label = smart_unicode(label) + self.required, self.label = required, label widget = widget or self.widget if isinstance(widget, type): widget = widget() + + # Hook into self.widget_attrs() for any Field-specific HTML attributes. + extra_attrs = self.widget_attrs(widget) + if extra_attrs: + widget.attrs.update(extra_attrs) + self.widget = widget # Increase the creation counter, and save our local copy. @@ -50,36 +60,62 @@ class Field(object): Raises ValidationError for any errors. """ if self.required and value in EMPTY_VALUES: - raise ValidationError(u'This field is required.') + raise ValidationError(gettext(u'This field is required.')) return value + def widget_attrs(self, widget): + """ + Given a Widget instance (*not* a Widget class), returns a dictionary of + any HTML attributes that should be added to the Widget, based on this + Field. + """ + return {} + class CharField(Field): - def __init__(self, max_length=None, min_length=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): self.max_length, self.min_length = max_length, min_length + Field.__init__(self, required, widget, label) def clean(self, value): "Validates max_length and min_length. Returns a Unicode object." Field.clean(self, value) - if value in EMPTY_VALUES: value = u'' + if value in EMPTY_VALUES: + value = u'' + if not self.required: + return value value = smart_unicode(value) if self.max_length is not None and len(value) > self.max_length: - raise ValidationError(u'Ensure this value has at most %d characters.' % self.max_length) + raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) if self.min_length is not None and len(value) < self.min_length: - raise ValidationError(u'Ensure this value has at least %d characters.' % self.min_length) + raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) return value + def widget_attrs(self, widget): + if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + return {'maxlength': str(self.max_length)} + class IntegerField(Field): + def __init__(self, max_value=None, min_value=None, required=True, widget=None, label=None): + self.max_value, self.min_value = max_value, min_value + Field.__init__(self, required, widget, label) + def clean(self, value): """ Validates that int() can be called on the input. Returns the result of int(). """ super(IntegerField, self).clean(value) + if not self.required and value in EMPTY_VALUES: + return u'' try: - return int(value) + value = int(value) except (ValueError, TypeError): - raise ValidationError(u'Enter a whole number.') + raise ValidationError(gettext(u'Enter a whole number.')) + if self.max_value is not None and value > self.max_value: + raise ValidationError(gettext(u'Ensure this value is less than or equal to %s.') % self.max_value) + if self.min_value is not None and value < self.min_value: + raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) + return value DEFAULT_DATE_INPUT_FORMATS = ( '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' @@ -90,8 +126,8 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) class DateField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS def clean(self, value): @@ -111,7 +147,34 @@ class DateField(Field): return datetime.date(*time.strptime(value, format)[:3]) except ValueError: continue - raise ValidationError(u'Enter a valid date.') + raise ValidationError(gettext(u'Enter a valid date.')) + +DEFAULT_TIME_INPUT_FORMATS = ( + '%H:%M:%S', # '14:30:59' + '%H:%M', # '14:30' +) + +class TimeField(Field): + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) + self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS + + def clean(self, value): + """ + Validates that the input can be converted to a time. Returns a Python + datetime.time object. + """ + Field.clean(self, value) + if value in EMPTY_VALUES: + return None + if isinstance(value, datetime.time): + return value + for format in self.input_formats: + try: + return datetime.time(*time.strptime(value, format)[3:6]) + except ValueError: + continue + raise ValidationError(gettext(u'Enter a valid time.')) DEFAULT_DATETIME_INPUT_FORMATS = ( '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' @@ -126,8 +189,8 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( ) class DateTimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS def clean(self, value): @@ -147,20 +210,22 @@ class DateTimeField(Field): return datetime.datetime(*time.strptime(value, format)[:6]) except ValueError: continue - raise ValidationError(u'Enter a valid date/time.') + raise ValidationError(gettext(u'Enter a valid date/time.')) class RegexField(Field): - def __init__(self, regex, error_message=None, required=True, widget=None): + def __init__(self, regex, max_length=None, min_length=None, error_message=None, + required=True, widget=None, label=None): """ regex can be either a string or a compiled regular expression object. error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ - Field.__init__(self, required, widget) + Field.__init__(self, required, widget, label) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex - self.error_message = error_message or u'Enter a valid value.' + self.max_length, self.min_length = max_length, min_length + self.error_message = error_message or gettext(u'Enter a valid value.') def clean(self, value): """ @@ -170,6 +235,12 @@ class RegexField(Field): Field.clean(self, value) if value in EMPTY_VALUES: value = u'' value = smart_unicode(value) + if not self.required and value == u'': + return value + if self.max_length is not None and len(value) > self.max_length: + raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) + if self.min_length is not None and len(value) < self.min_length: + raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) if not self.regex.search(value): raise ValidationError(self.error_message) return value @@ -180,8 +251,8 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): - def __init__(self, required=True, widget=None): - RegexField.__init__(self, email_re, u'Enter a valid e-mail address.', required, widget) + def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): + RegexField.__init__(self, email_re, max_length, min_length, gettext(u'Enter a valid e-mail address.'), required, widget, label) url_re = re.compile( r'^https?://' # http:// or https:// @@ -197,9 +268,9 @@ except ImportError: URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' class URLField(RegexField): - def __init__(self, required=True, verify_exists=False, widget=None, + def __init__(self, max_length=None, min_length=None, required=True, verify_exists=False, widget=None, label=None, validator_user_agent=URL_VALIDATOR_USER_AGENT): - RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget) + RegexField.__init__(self, url_re, max_length, min_length, gettext(u'Enter a valid URL.'), required, widget, label) self.verify_exists = verify_exists self.user_agent = validator_user_agent @@ -219,9 +290,9 @@ class URLField(RegexField): req = urllib2.Request(value, None, headers) u = urllib2.urlopen(req) except ValueError: - raise ValidationError(u'Enter a valid URL.') + raise ValidationError(gettext(u'Enter a valid URL.')) except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError(u'This URL appears to be a broken link.') + raise ValidationError(gettext(u'This URL appears to be a broken link.')) return value class BooleanField(Field): @@ -233,10 +304,10 @@ class BooleanField(Field): return bool(value) class ChoiceField(Field): - def __init__(self, choices=(), required=True, widget=Select): + def __init__(self, choices=(), required=True, widget=Select, label=None): if isinstance(widget, type): widget = widget(choices=choices) - Field.__init__(self, required, widget) + Field.__init__(self, required, widget, label) self.choices = choices def clean(self, value): @@ -246,37 +317,46 @@ class ChoiceField(Field): value = Field.clean(self, value) if value in EMPTY_VALUES: value = u'' value = smart_unicode(value) + if not self.required and value == u'': + return value valid_values = set([str(k) for k, v in self.choices]) if value not in valid_values: - raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % value) + raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % value) return value class MultipleChoiceField(ChoiceField): - def __init__(self, choices=(), required=True, widget=SelectMultiple): - ChoiceField.__init__(self, choices, required, widget) + def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None): + ChoiceField.__init__(self, choices, required, widget, label) def clean(self, value): """ Validates that the input is a list or tuple. """ - if not isinstance(value, (list, tuple)): - raise ValidationError(u'Enter a list of values.') if self.required and not value: - raise ValidationError(u'This field is required.') + raise ValidationError(gettext(u'This field is required.')) + elif not self.required and not value: + return [] + if not isinstance(value, (list, tuple)): + raise ValidationError(gettext(u'Enter a list of values.')) new_value = [] for val in value: val = smart_unicode(val) new_value.append(val) # Validate that each value in the value list is in self.choices. - valid_values = set([k for k, v in self.choices]) + valid_values = set([smart_unicode(k) for k, v in self.choices]) for val in new_value: if val not in valid_values: - raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % val) + raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val) return new_value class ComboField(Field): - def __init__(self, fields=(), required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, fields=(), required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) + # Set 'required' to False on the individual fields, because the + # required validation will be handled by ComboField, not by those + # individual fields. + for f in fields: + f.required = False self.fields = fields def clean(self, value): diff --git a/django/newforms/forms.py b/django/newforms/forms.py index b8264fb691..201cce3868 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -2,10 +2,13 @@ Form classes """ -from django.utils.datastructures import SortedDict +from django.utils.datastructures import SortedDict, MultiValueDict +from django.utils.html import escape from fields import Field -from widgets import TextInput, Textarea -from util import ErrorDict, ErrorList, ValidationError +from widgets import TextInput, Textarea, HiddenInput +from util import StrAndUnicode, ErrorDict, ErrorList, ValidationError + +__all__ = ('BaseForm', 'Form') NON_FIELD_ERRORS = '__all__' @@ -31,17 +34,20 @@ class DeclarativeFieldsMetaclass(type): attrs['fields'] = SortedDictFromList(fields) return type.__new__(cls, name, bases, attrs) -class Form(object): - "A collection of Fields, plus their associated data." - __metaclass__ = DeclarativeFieldsMetaclass - - def __init__(self, data=None, auto_id=False): # TODO: prefix stuff +class BaseForm(StrAndUnicode): + # This is the main implementation of all the Form logic. Note that this + # class is different than Form. See the comments by the Form class for more + # information. Any improvements to the form API should be made to *this* + # class, not to the Form class. + def __init__(self, data=None, auto_id='id_%s', prefix=None): + self.ignore_errors = data is None self.data = data or {} self.auto_id = auto_id + self.prefix = prefix self.clean_data = None # Stores the data after clean() has been called. self.__errors = None # Stores the errors after clean() has been called. - def __str__(self): + def __unicode__(self): return self.as_table() def __iter__(self): @@ -56,60 +62,76 @@ class Form(object): raise KeyError('Key %r not found in Form' % name) return BoundField(self, field, name) - def clean(self): - if self.__errors is None: - self.full_clean() - return self.clean_data - - def errors(self): + def _errors(self): "Returns an ErrorDict for self.data" if self.__errors is None: self.full_clean() return self.__errors + errors = property(_errors) def is_valid(self): """ - Returns True if the form has no errors. Otherwise, False. This exists - solely for convenience, so client code can use positive logic rather - than confusing negative logic ("if not form.errors()"). + Returns True if the form has no errors. Otherwise, False. If errors are + being ignored, returns False. + """ + return not self.ignore_errors and not bool(self.errors) + + def add_prefix(self, field_name): + """ + Returns the field name with a prefix appended, if this Form has a + prefix set. + + Subclasses may wish to override. """ - return not bool(self.errors()) + return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name + + def _html_output(self, normal_row, error_row, row_ender, errors_on_separate_row): + "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." + top_errors = self.non_field_errors() # Errors that should be displayed above all fields. + output, hidden_fields = [], [] + for name, field in self.fields.items(): + bf = BoundField(self, field, name) + bf_errors = bf.errors # Cache in local variable. + if bf.is_hidden: + if bf_errors: + top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors]) + hidden_fields.append(unicode(bf)) + else: + if errors_on_separate_row and bf_errors: + output.append(error_row % bf_errors) + label = bf.label and bf.label_tag(escape(bf.label + ':')) or '' + output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': bf}) + if top_errors: + output.insert(0, error_row % top_errors) + if hidden_fields: # Insert any hidden fields in the last row. + str_hidden = u''.join(hidden_fields) + if output: + last_row = output[-1] + # Chop off the trailing row_ender (e.g. '</td></tr>') and insert the hidden fields. + output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender + else: # If there aren't any rows in the output, just append the hidden fields. + output.append(str_hidden) + return u'\n'.join(output) def as_table(self): "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." - return u'\n'.join(['<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', False) def as_ul(self): "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." - return u'\n'.join(['<li>%s: %s</li>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'<li>%(errors)s%(label)s %(field)s</li>', u'<li>%s</li>', '</li>', False) - def as_table_with_errors(self): - "Returns this form rendered as HTML <tr>s, with errors." - output = [] - if self.errors().get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]])) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - if bf.errors: - output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors])) - output.append('<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), bf)) - return u'\n'.join(output) + def as_p(self): + "Returns this form rendered as HTML <p>s." + return self._html_output(u'<p>%(label)s %(field)s</p>', u'<p>%s</p>', '</p>', True) - def as_ul_with_errors(self): - "Returns this form rendered as HTML <li>s, with errors." - output = [] - if self.errors().get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append('<li><ul>%s</ul></li>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]])) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - line = '<li>' - if bf.errors: - line += '<ul>%s</ul>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors]) - line += '%s: %s</li>' % (pretty_name(name), bf) - output.append(line) - return u'\n'.join(output) + def non_field_errors(self): + """ + Returns an ErrorList of errors that aren't associated with a particular + field -- i.e., from Form.clean(). Returns an empty ErrorList if there + are none. + """ + return self.errors.get(NON_FIELD_ERRORS, ErrorList()) def full_clean(self): """ @@ -117,8 +139,14 @@ class Form(object): """ self.clean_data = {} errors = ErrorDict() + if self.ignore_errors: # Stop further processing. + self.__errors = errors + return for name, field in self.fields.items(): - value = self.data.get(name, None) + # value_from_datadict() gets the data from the dictionary. + # Each widget type knows how to retrieve its own data, because some + # widgets split data over several HTML fields. + value = field.widget.value_from_datadict(self.data, self.add_prefix(name)) try: value = field.clean(value) self.clean_data[name] = value @@ -138,40 +166,59 @@ class Form(object): def clean(self): """ Hook for doing any extra form-wide cleaning after Field.clean() been - called on every field. + called on every field. Any ValidationError raised by this method will + not be associated with a particular field; it will have a special-case + association with the field named '__all__'. """ return self.clean_data -class BoundField(object): +class Form(BaseForm): + "A collection of Fields, plus their associated data." + # This is a separate class from BaseForm in order to abstract the way + # self.fields is specified. This class (Form) is the one that does the + # fancy metaclass stuff purely for the semantic sugar -- it allows one + # to define a form using declarative syntax. + # BaseForm itself has no way of designating self.fields. + __metaclass__ = DeclarativeFieldsMetaclass + +class BoundField(StrAndUnicode): "A Field plus data" def __init__(self, form, field, name): - self._form = form - self._field = field - self._name = name + self.form = form + self.field = field + self.name = name + self.html_name = form.add_prefix(name) + if self.field.label is None: + self.label = pretty_name(name) + else: + self.label = self.field.label - def __str__(self): + def __unicode__(self): "Renders this field as an HTML widget." # Use the 'widget' attribute on the field to determine which type # of HTML widget to use. - return self.as_widget(self._field.widget) + value = self.as_widget(self.field.widget) + if not isinstance(value, basestring): + # Some Widget render() methods -- notably RadioSelect -- return a + # "special" object rather than a string. Call the __str__() on that + # object to get its rendered value. + value = value.__str__() + return value def _errors(self): """ Returns an ErrorList for this field. Returns an empty ErrorList if there are none. """ - try: - return self._form.errors()[self._name] - except KeyError: - return ErrorList() + return self.form.errors.get(self.name, ErrorList()) errors = property(_errors) def as_widget(self, widget, attrs=None): attrs = attrs or {} auto_id = self.auto_id - if not attrs.has_key('id') and not widget.attrs.has_key('id') and auto_id: + if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'): attrs['id'] = auto_id - return widget.render(self._name, self._form.data.get(self._name, None), attrs=attrs) + return widget.render(self.html_name, self.data, attrs=attrs) def as_text(self, attrs=None): """ @@ -183,15 +230,44 @@ class BoundField(object): "Returns a string of HTML for representing this as a <textarea>." return self.as_widget(Textarea(), attrs) + def as_hidden(self, attrs=None): + """ + Returns a string of HTML for representing this as an <input type="hidden">. + """ + return self.as_widget(HiddenInput(), attrs) + + def _data(self): + "Returns the data for this BoundField, or None if it wasn't given." + return self.field.widget.value_from_datadict(self.form.data, self.html_name) + data = property(_data) + + def label_tag(self, contents=None): + """ + Wraps the given contents in a <label>, if the field has an ID attribute. + Does not HTML-escape the contents. If contents aren't given, uses the + field's HTML-escaped label. + """ + contents = contents or escape(self.label) + widget = self.field.widget + id_ = widget.attrs.get('id') or self.auto_id + if id_: + contents = '<label for="%s">%s</label>' % (widget.id_for_label(id_), contents) + return contents + + def _is_hidden(self): + "Returns True if this BoundField's widget is hidden." + return self.field.widget.is_hidden + is_hidden = property(_is_hidden) + def _auto_id(self): """ Calculates and returns the ID attribute for this BoundField, if the associated Form has specified auto_id. Returns an empty string otherwise. """ - auto_id = self._form.auto_id + auto_id = self.form.auto_id if auto_id and '%s' in str(auto_id): - return str(auto_id) % self._name + return str(auto_id) % self.html_name elif auto_id: - return self._name + return self.html_name return '' auto_id = property(_auto_id) diff --git a/django/newforms/models.py b/django/newforms/models.py new file mode 100644 index 0000000000..6b111d7ee1 --- /dev/null +++ b/django/newforms/models.py @@ -0,0 +1,38 @@ +""" +Helper functions for creating Form classes from Django models +and database field objects. +""" + +from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList + +__all__ = ('form_for_model', 'form_for_fields') + +def create(self, save=True): + "Creates and returns model instance according to self.clean_data." + if self.errors: + raise ValueError("The %s could not be created because the data didn't validate." % self._model._meta.object_name) + obj = self._model(**self.clean_data) + if save: + obj.save() + return obj + +def form_for_model(model, form=None): + """ + Returns a Form class for the given Django model class. + + Provide 'form' if you want to use a custom BaseForm subclass. + """ + opts = model._meta + field_list = [] + for f in opts.fields + opts.many_to_many: + formfield = f.formfield() + if formfield: + field_list.append((f.name, formfield)) + fields = SortedDictFromList(field_list) + form = form or BaseForm + return type(opts.object_name + 'Form', (form,), {'fields': fields, '_model': model, 'create': create}) + +def form_for_fields(field_list): + "Returns a Form class for the given list of Django database field instances." + fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list]) + return type('FormForFields', (BaseForm,), {'fields': fields}) diff --git a/django/newforms/util.py b/django/newforms/util.py index a5cc4932ea..a78623a17b 100644 --- a/django/newforms/util.py +++ b/django/newforms/util.py @@ -1,13 +1,22 @@ -# Default encoding for input byte strings. -DEFAULT_ENCODING = 'utf-8' # TODO: First look at django.conf.settings, then fall back to this. +from django.conf import settings def smart_unicode(s): if not isinstance(s, basestring): s = unicode(str(s)) elif not isinstance(s, unicode): - s = unicode(s, DEFAULT_ENCODING) + s = unicode(s, settings.DEFAULT_CHARSET) return s +class StrAndUnicode(object): + """ + A class whose __str__ returns its __unicode__ as a bytestring + according to settings.DEFAULT_CHARSET. + + Useful as a mix-in. + """ + def __str__(self): + return self.__unicode__().encode(settings.DEFAULT_CHARSET) + class ErrorDict(dict): """ A collection of errors that knows how to display itself in various formats. diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index 318c76e55d..996e353775 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -5,10 +5,11 @@ HTML Widget classes __all__ = ( 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'FileInput', 'Textarea', 'CheckboxInput', - 'Select', 'SelectMultiple', 'RadioSelect', + 'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', ) -from util import smart_unicode +from util import StrAndUnicode, smart_unicode +from django.utils.datastructures import MultiValueDict from django.utils.html import escape from itertools import chain @@ -22,25 +23,54 @@ except NameError: flatatt = lambda attrs: u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()]) class Widget(object): - requires_data_list = False # Determines whether render()'s 'value' argument should be a list. + is_hidden = False # Determines whether this corresponds to an <input type="hidden">. + def __init__(self, attrs=None): self.attrs = attrs or {} - def render(self, name, value): + def render(self, name, value, attrs=None): + """ + Returns this Widget rendered as HTML, as a Unicode string. + + The 'value' given is not guaranteed to be valid input, so subclass + implementations should program defensively. + """ raise NotImplementedError def build_attrs(self, extra_attrs=None, **kwargs): + "Helper function for building an attribute dictionary." attrs = dict(self.attrs, **kwargs) if extra_attrs: attrs.update(extra_attrs) return attrs + def value_from_datadict(self, data, name): + """ + Given a dictionary of data and this widget's name, returns the value + of this widget. Returns None if it's not provided. + """ + return data.get(name, None) + + def id_for_label(self, id_): + """ + Returns the HTML ID attribute of this Widget for use by a <label>, + given the ID of the field. Returns None if no ID is available. + + This hook is necessary because some widgets have multiple HTML + elements and, thus, multiple IDs. In that case, this method should + return an ID value that corresponds to the first ID in the widget's + tags. + """ + return id_ + id_for_label = classmethod(id_for_label) + class Input(Widget): """ Base class for all <input> widgets (except type='checkbox' and type='radio', which are special). """ input_type = None # Subclasses must define this. + def render(self, name, value, attrs=None): if value is None: value = '' final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) @@ -55,6 +85,7 @@ class PasswordInput(Input): class HiddenInput(Input): input_type = 'hidden' + is_hidden = True class FileInput(Input): input_type = 'file' @@ -67,9 +98,22 @@ class Textarea(Widget): return u'<textarea%s>%s</textarea>' % (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'<input%s />' % 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'</select>') return u'\n'.join(output) -class RadioInput(object): + def value_from_datadict(self, data, name): + if isinstance(data, MultiValueDict): + return data.getlist(name) + return data.get(name, None) + +class RadioInput(StrAndUnicode): "An object used by RadioFieldRenderer that represents a single <input type='radio'>." - def __init__(self, name, value, attrs, choice): + def __init__(self, name, value, attrs, choice, index): self.name, self.value = name, value - self.attrs = attrs or {} + self.attrs = attrs self.choice_value, self.choice_label = choice + self.index = index - def __str__(self): + def __unicode__(self): return u'<label>%s %s</label>' % (self.tag(), self.choice_label) def is_checked(self): 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'<input%s />' % flatatt(final_attrs) -class RadioFieldRenderer(object): +class RadioFieldRenderer(StrAndUnicode): "An object used by RadioSelect to enable customization of radio widgets." def __init__(self, name, value, attrs, choices): self.name, self.value, self.attrs = name, value, attrs 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 __getitem__(self, idx): + choice = self.choices[idx] # Let the IndexError propogate + return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx) - def __str__(self): + def __unicode__(self): "Outputs a <ul> for this set of radio fields." return u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' % w for w in self]) @@ -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 <input type="radio"> 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'<ul>'] + 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'<li><label>%s %s</label></li>' % (rendered_cb, escape(smart_unicode(option_label)))) + output.append(u'</ul>') + 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 '<FormFieldWrapper for "%s">' % 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 '<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.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 "<FormFieldCollection: %s>" % 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 '<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \ + (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 '<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \ + (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 '<input type="hidden" id="%s" name="%s" value="%s" />' % \ + (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 '<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \ + (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 = ['<select id="%s" class="v%s%s" name="%s" size="%s">' % \ + (self.get_id(), self.__class__.__name__, + self.is_required and ' required' or '', self.field_name, self.size)] + str_data = str(data) # normalize to string + for value, display_name in self.choices: + selected_html = '' + if str(value) == str_data: + selected_html = ' selected="selected"' + output.append(' <option value="%s"%s>%s</option>' % (escape(value), selected_html, escape(display_name))) + output.append(' </select>') + 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 }}<br /> + {% 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 <ul>" + output = ['<ul%s>' % (self.ul_class and ' class="%s"' % self.ul_class or '')] + output.extend(['<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist]) + output.append('</ul>') + 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': '<input type="radio" id="%s" name="%s" value="%s"%s/>' % \ + (self.get_id() + '_' + str(i), self.field_name, value, selected_html), + 'label': '<label for="%s">%s</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 = ['<select id="%s" class="v%s%s" name="%s" size="%s" multiple="multiple">' % \ + (self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', + self.field_name, self.size)] + str_data_list = map(str, data) # normalize to strings + for value, choice in self.choices: + selected_html = '' + if str(value) in str_data_list: + selected_html = ' selected="selected"' + output.append(' <option value="%s"%s>%s</option>' % (escape(value), selected_html, escape(choice))) + output.append(' </select>') + 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 <select multiple>, this widget outputs a + <ul> of <input type="checkbox">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 = ['<ul%s>' % (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('<li><input type="checkbox" id="%s" class="v%s" name="%s"%s value="on" /> <label for="%s">%s</label></li>' % \ + (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, + self.get_id() + escape(value), choice)) + output.append('</ul>') + 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 '<input type="file" id="%s" class="v%s" name="%s" />' % \ + (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: ``<app_label>/<model_name>_detail.html`` Context: |
