diff --git a/AUTHORS b/AUTHORS index b291f22400..dbbf6e7bad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -51,6 +51,7 @@ answer newbie questions, and generally made Django that much better: Jiri Barton Ned Batchelder Shannon -jj Behrens + Esdras Beleza James Bennett Paul Bissex Simon Blanchard @@ -150,6 +151,7 @@ answer newbie questions, and generally made Django that much better: SmileyChris sopel Thomas Steinacher + nowell strite Radek Švarz Swaroop C H Aaron Swartz 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/media/js/admin/DateTimeShortcuts.js b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js index 77c536b865..b1504fc819 100644 --- a/django/contrib/admin/media/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js @@ -44,7 +44,7 @@ var DateTimeShortcuts = { var shortcuts_span = document.createElement('span'); inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); var now_link = document.createElement('a'); - now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().getHourMinute());"); + now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().getHourMinuteSecond());"); now_link.appendChild(document.createTextNode(gettext('Now'))); var clock_link = document.createElement('a'); clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');'); @@ -80,10 +80,10 @@ var DateTimeShortcuts = { quickElement('h2', clock_box, gettext('Choose a time')); time_list = quickElement('ul', clock_box, ''); time_list.className = 'timelist'; - quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().getHourMinute());") - quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '00:00');") - quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '06:00');") - quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '12:00');") + quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().getHourMinuteSecond());") + quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '00:00:00');") + quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '06:00:00');") + quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '12:00:00');") cancel_p = quickElement('p', clock_box, ''); cancel_p.className = 'calendar-cancel'; diff --git a/django/contrib/admin/media/js/core.js b/django/contrib/admin/media/js/core.js index d35bd29c1c..a17ac8a4d2 100644 --- a/django/contrib/admin/media/js/core.js +++ b/django/contrib/admin/media/js/core.js @@ -119,6 +119,10 @@ Date.prototype.getTwoDigitMinute = function() { return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); } +Date.prototype.getTwoDigitSecond = function() { + return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); +} + Date.prototype.getISODate = function() { return this.getCorrectYear() + '-' + this.getTwoDigitMonth() + '-' + this.getTwoDigitDate(); } @@ -127,6 +131,10 @@ Date.prototype.getHourMinute = function() { return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute(); } +Date.prototype.getHourMinuteSecond = function() { + return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond(); +} + // ---------------------------------------------------------------------------- // String object extensions // ---------------------------------------------------------------------------- diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html index d9126c3ec5..445cca3089 100644 --- a/django/contrib/admin/templates/admin/search_form.html +++ b/django/contrib/admin/templates/admin/search_form.html @@ -7,7 +7,7 @@ {% if show_result_count %} - {% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} ({% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}) + {% blocktrans count cl.result_count as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %} ({% blocktrans with cl.full_result_count as full_result_count %}{{ full_result_count }} total{% endblocktrans %}) {% endif %} {% for pair in cl.params.items %} {% ifnotequal pair.0 search_var %}{% endifnotequal %} diff --git a/django/contrib/admin/views/auth.py b/django/contrib/admin/views/auth.py index 03876bb4ac..abc02e96c4 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 324841a669..548f8b9687 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 @@ -226,7 +226,7 @@ index = staff_member_required(never_cache(index)) def add_stage(request, app_label, model_name, show_delete=False, form_url='', post_url=None, post_url_continue='../%s/', object_id_override=None): model = models.get_model(app_label, model_name) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) opts = model._meta if not request.user.has_perm(app_label + '.' + opts.get_add_permission()): @@ -283,7 +283,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, @@ -302,7 +302,7 @@ def change_stage(request, app_label, model_name, object_id): model = models.get_model(app_label, model_name) object_id = unquote(object_id) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) opts = model._meta if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): @@ -313,8 +313,8 @@ def change_stage(request, app_label, model_name, object_id): try: manipulator = model.ChangeManipulator(object_id) - except ObjectDoesNotExist: - raise Http404 + except model.DoesNotExist: + raise Http404('%s object with primary key %r does not exist' % (model_name, escape(object_id))) if request.POST: new_data = request.POST.copy() @@ -374,7 +374,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 = [] @@ -490,7 +490,7 @@ def delete_stage(request, app_label, model_name, object_id): model = models.get_model(app_label, model_name) object_id = unquote(object_id) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) opts = model._meta if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()): raise PermissionDenied @@ -527,7 +527,7 @@ def history(request, app_label, model_name, object_id): model = models.get_model(app_label, model_name) object_id = unquote(object_id) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) action_list = LogEntry.objects.filter(object_id=object_id, content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time') # If no history was found, see whether this object even exists. @@ -743,7 +743,7 @@ class ChangeList(object): def change_list(request, app_label, model_name): model = models.get_model(app_label, model_name) if model is None: - raise Http404, "App %r, model %r, not found" % (app_label, model_name) + raise Http404("App %r, model %r, not found" % (app_label, model_name)) if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()): raise PermissionDenied try: diff --git a/django/contrib/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/formtools/__init__.py b/django/contrib/formtools/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/formtools/preview.py b/django/contrib/formtools/preview.py new file mode 100644 index 0000000000..9a9371b5f8 --- /dev/null +++ b/django/contrib/formtools/preview.py @@ -0,0 +1,160 @@ +""" +Formtools Preview application. + +This is an abstraction of the following workflow: + + "Display an HTML form, force a preview, then do something with the submission." + +Given a django.newforms.Form object that you define, this takes care of the +following: + + * Displays the form as HTML on a Web page. + * Validates the form data once it's submitted via POST. + * If it's valid, displays a preview page. + * If it's not valid, redisplays the form with error messages. + * At the preview page, if the preview confirmation button is pressed, calls + a hook that you define -- a done() method. + +The framework enforces the required preview by passing a shared-secret hash to +the preview page. If somebody tweaks the form parameters on the preview page, +the form submission will fail the hash comparison test. + +Usage +===== + +Subclass FormPreview and define a done() method: + + def done(self, request, clean_data): + # ... + +This method takes an HttpRequest object and a dictionary of the form data after +it has been validated and cleaned. It should return an HttpResponseRedirect. + +Then, just instantiate your FormPreview subclass by passing it a Form class, +and pass that to your URLconf, like so: + + (r'^post/$', MyFormPreview(MyForm)), + +The FormPreview class has a few other hooks. See the docstrings in the source +code below. + +The framework also uses two templates: 'formtools/preview.html' and +'formtools/form.html'. You can override these by setting 'preview_template' and +'form_template' attributes on your FormPreview subclass. See +django/contrib/formtools/templates for the default templates. +""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404 +from django.shortcuts import render_to_response +import cPickle as pickle +import md5 + +AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. + +class FormPreview(object): + preview_template = 'formtools/preview.html' + form_template = 'formtools/form.html' + + # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### + + def __init__(self, form): + # form should be a Form class, not an instance. + self.form, self.state = form, {} + + def __call__(self, request, *args, **kwargs): + stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview') + self.parse_params(*args, **kwargs) + try: + method = getattr(self, stage + '_' + request.method.lower()) + except AttributeError: + raise Http404 + return method(request) + + def unused_name(self, name): + """ + Given a first-choice name, adds an underscore to the name until it + reaches a name that isn't claimed by any field in the form. + + This is calculated rather than being hard-coded so that no field names + are off-limits for use in the form. + """ + while 1: + try: + f = self.form.fields[name] + except KeyError: + break # This field name isn't being used by the form. + name += '_' + return name + + def preview_get(self, request): + "Displays the form" + f = self.form(auto_id=AUTO_ID) + return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}) + + def preview_post(self, request): + "Validates the POST data. If valid, displays the preview page. Else, redisplays form." + f = self.form(request.POST, auto_id=AUTO_ID) + context = {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state} + if f.is_valid(): + context['hash_field'] = self.unused_name('hash') + context['hash_value'] = self.security_hash(request, f) + return render_to_response(self.preview_template, context) + else: + return render_to_response(self.form_template, context) + + def post_post(self, request): + "Validates the POST data. If valid, calls done(). Else, redisplays form." + f = self.form(request.POST, auto_id=AUTO_ID) + if f.is_valid(): + if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')): + return self.failed_hash(request) # Security hash failed. + return self.done(request, f.clean_data) + else: + return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}) + + # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## + + def parse_params(self, *args, **kwargs): + """ + Given captured args and kwargs from the URLconf, saves something in + self.state and/or raises Http404 if necessary. + + For example, this URLconf captures a user_id variable: + + (r'^contact/(?P\d{1,6})/$', MyFormPreview(MyForm)), + + In this case, the kwargs variable in parse_params would be + {'user_id': 32} for a request to '/contact/32/'. You can use that + user_id to make sure it's a valid user and/or save it for later, for + use in done(). + """ + pass + + def security_hash(self, request, form): + """ + Calculates the security hash for the given Form instance. + + This creates a list of the form field names/values in a deterministic + order, pickles the result with the SECRET_KEY setting and takes an md5 + hash of that. + + Subclasses may want to take into account request-specific information + such as the IP address. + """ + data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY] + # Use HIGHEST_PROTOCOL because it's the most efficient. It requires + # Python 2.3, but Django requires 2.3 anyway, so that's OK. + pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL) + return md5.new(pickled).hexdigest() + + def failed_hash(self, request): + "Returns an HttpResponse in the case of an invalid security hash." + return self.preview_post(request) + + # METHODS SUBCLASSES MUST OVERRIDE ######################################## + + def done(self, request, clean_data): + "Does something with the clean_data and returns an HttpResponseRedirect." + raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__) diff --git a/django/contrib/formtools/templates/formtools/form.html b/django/contrib/formtools/templates/formtools/form.html new file mode 100644 index 0000000000..90da8b2b2b --- /dev/null +++ b/django/contrib/formtools/templates/formtools/form.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block content %} + +{% if form.errors %}

Please correct the following errors

{% else %}

Submit

{% endif %} + +
+ +{{ form }} +
+ +

+
+ +{% endblock %} diff --git a/django/contrib/formtools/templates/formtools/preview.html b/django/contrib/formtools/templates/formtools/preview.html new file mode 100644 index 0000000000..c7955d46e1 --- /dev/null +++ b/django/contrib/formtools/templates/formtools/preview.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} + +

Preview your submission

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

Security hash: {{ hash_value }}

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

+
+ +

Or edit it again

+ +
+ +{{ form }} +
+ +

+
+ +{% endblock %} diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 2c76e13c22..44ede4460a 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -29,7 +29,7 @@ def ping_google(sitemap_url=None, ping_url=PING_URL): from django.contrib.sites.models import Site current_site = Site.objects.get_current() - url = "%s%s" % (current_site.domain, sitemap) + url = "%s%s" % (current_site.domain, sitemap_url) params = urllib.urlencode({'sitemap':url}) urllib.urlopen("%s?%s" % (ping_url, params)) diff --git a/django/contrib/sitemaps/templates/sitemap.xml b/django/contrib/sitemaps/templates/sitemap.xml index ad24c045d4..16d9a0bbe0 100644 --- a/django/contrib/sitemaps/templates/sitemap.xml +++ b/django/contrib/sitemaps/templates/sitemap.xml @@ -1,5 +1,5 @@ - + {% spaceless %} {% for url in urlset %} diff --git a/django/contrib/sitemaps/templates/sitemap_index.xml b/django/contrib/sitemaps/templates/sitemap_index.xml index c89b192ecc..a2bcce85dc 100644 --- a/django/contrib/sitemaps/templates/sitemap_index.xml +++ b/django/contrib/sitemaps/templates/sitemap_index.xml @@ -1,4 +1,4 @@ - + {% for location in sitemaps %}{{ location|escape }}{% endfor %} diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index c1403ea4fa..85473a6353 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -84,7 +84,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 2998bd31f6..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,7 +157,11 @@ class WSGIRequest(http.HttpRequest): return self._raw_post_data except AttributeError: buf = StringIO() - content_length = int(self.environ['CONTENT_LENGTH']) + 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/core/xheaders.py b/django/core/xheaders.py index 69f6115839..3beb930158 100644 --- a/django/core/xheaders.py +++ b/django/core/xheaders.py @@ -17,6 +17,6 @@ def populate_xheaders(request, response, model, object_id): or if the request is from a logged in staff member. """ from django.conf import settings - if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or (request.user.is_authenticated() and request.user.is_staff): + if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or (hasattr(request, 'user') and request.user.is_authenticated() and request.user.is_staff): response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.object_name.lower()) response['X-Object-Id'] = str(object_id) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index d59b697de2..d4dd4cdd47 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -2,7 +2,7 @@ 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.core.exceptions import ObjectDoesNotExist from django.utils.functional import curry from django.utils.itercompat import tee @@ -210,10 +210,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: @@ -222,7 +222,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. @@ -337,6 +337,12 @@ class Field(object): return self._choices choices = property(_get_choices) + def formfield(self): + "Returns a django.newforms.Field instance for this database Field." + from django.newforms import CharField + # TODO: This is just a temporary default during development. + return CharField(label=capfirst(self.verbose_name)) + class AutoField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): @@ -358,7 +364,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 @@ -385,11 +391,11 @@ 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] class CharField(Field): def get_manipulator_field_objs(self): - return [forms.TextField] + return [oldforms.TextField] def to_python(self, value): if isinstance(value, basestring): @@ -404,7 +410,7 @@ class CharField(Field): # 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 @@ -472,7 +478,7 @@ class DateField(Field): 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) @@ -497,7 +503,7 @@ 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/Oracle will throw a warning if microseconds are given, because # neither database supports microseconds. if settings.DATABASE_ENGINE in ('mysql', 'oracle') and hasattr(value, 'microsecond'): @@ -505,14 +511,6 @@ class DateTimeField(DateField): # cx_Oracle wants the raw datetime instead of a string. if settings.DATABASE_ENGINE != 'oracle': value = str(value) - elif isinstance(value, datetime.date): - # MySQL/Oracle will throw a warning if microseconds are given, because - # neither database supports microseconds. - if settings.DATABASE_ENGINE in ('mysql', 'oracle') and hasattr(value, 'microsecond'): - value = datetime.datetime(value.year, value.month, value.day, microsecond=0) - # cx_Oracle wants the raw datetime instead of a string. - if settings.DATABASE_ENGINE != 'oracle': - value = str(value) return Field.get_db_prep_save(self, value) def get_db_prep_lookup(self, lookup_type, value): @@ -527,7 +525,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'] @@ -564,7 +562,7 @@ 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) @@ -628,7 +626,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] @@ -656,7 +654,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 @@ -665,7 +663,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): @@ -673,7 +671,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) @@ -699,7 +697,7 @@ class ImageField(FileField): class IntegerField(Field): empty_strings_allowed = False def get_manipulator_field_objs(self): - return [forms.IntegerField] + return [oldforms.IntegerField] class IPAddressField(Field): def __init__(self, *args, **kwargs): @@ -707,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) @@ -718,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): @@ -745,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 @@ -795,7 +793,7 @@ 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) @@ -808,11 +806,11 @@ class URLField(Field): Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [forms.URLField] + return [oldforms.URLField] 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): @@ -823,7 +821,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 @@ -836,4 +834,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 ad78a01252..102e117631 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 @@ -493,13 +493,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 +508,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 +581,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 +622,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 c61e82f813..aea3aa70a7 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. @@ -308,7 +308,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/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/middleware/gzip.py b/django/middleware/gzip.py index 7d860abdb1..a7c74481d0 100644 --- a/django/middleware/gzip.py +++ b/django/middleware/gzip.py @@ -25,4 +25,5 @@ class GZipMiddleware(object): response.content = compress_string(response.content) response['Content-Encoding'] = 'gzip' + response['Content-Length'] = str(len(response.content)) return response diff --git a/django/newforms/__init__.py b/django/newforms/__init__.py index 2a472d7b39..a445a21bfb 100644 --- a/django/newforms/__init__.py +++ b/django/newforms/__init__.py @@ -14,15 +14,4 @@ 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 models import * diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 179555fc77..24849bdbcf 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', @@ -28,13 +30,26 @@ except NameError: class Field(object): widget = TextInput # Default widget to use when rendering this type of Field. - def __init__(self, required=True, widget=None): - self.required = required + # Tracks each time a Field instance is created. Used to retain order. + creation_counter = 0 + + def __init__(self, required=True, widget=None, label=None): + self.required, self.label = required, label widget = widget or self.widget if isinstance(widget, type): widget = widget() + + # Hook into self.widget_attrs() for any Field-specific HTML attributes. + extra_attrs = self.widget_attrs(widget) + if extra_attrs: + widget.attrs.update(extra_attrs) + self.widget = widget + # Increase the creation counter, and save our local copy. + self.creation_counter = Field.creation_counter + Field.creation_counter += 1 + def clean(self, value): """ Validates the given value and returns its "cleaned" value as an @@ -43,13 +58,21 @@ class Field(object): Raises ValidationError for any errors. """ if self.required and value in EMPTY_VALUES: - raise ValidationError(u'This field is required.') + raise ValidationError(gettext(u'This field is required.')) return value + def widget_attrs(self, widget): + """ + Given a Widget instance (*not* a Widget class), returns a dictionary of + any HTML attributes that should be added to the Widget, based on this + Field. + """ + return {} + class CharField(Field): - def __init__(self, max_length=None, min_length=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): self.max_length, self.min_length = max_length, min_length + Field.__init__(self, required, widget, label) def clean(self, value): "Validates max_length and min_length. Returns a Unicode object." @@ -57,11 +80,15 @@ class CharField(Field): if value in EMPTY_VALUES: value = u'' value = smart_unicode(value) if self.max_length is not None and len(value) > self.max_length: - raise ValidationError(u'Ensure this value has at most %d characters.' % self.max_length) + raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) if self.min_length is not None and len(value) < self.min_length: - raise ValidationError(u'Ensure this value has at least %d characters.' % self.min_length) + raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) return value + def widget_attrs(self, widget): + if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + return {'maxlength': str(self.max_length)} + class IntegerField(Field): def clean(self, value): """ @@ -69,10 +96,12 @@ class IntegerField(Field): of int(). """ super(IntegerField, self).clean(value) + if not self.required and value in EMPTY_VALUES: + return u'' try: return int(value) except (ValueError, TypeError): - raise ValidationError(u'Enter a whole number.') + raise ValidationError(gettext(u'Enter a whole number.')) DEFAULT_DATE_INPUT_FORMATS = ( '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' @@ -83,8 +112,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): @@ -104,7 +133,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' @@ -119,8 +175,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): @@ -140,20 +196,20 @@ class DateTimeField(Field): return datetime.datetime(*time.strptime(value, format)[:6]) except ValueError: continue - raise ValidationError(u'Enter a valid date/time.') + raise ValidationError(gettext(u'Enter a valid date/time.')) class RegexField(Field): - def __init__(self, regex, error_message=None, required=True, widget=None): + def __init__(self, regex, error_message=None, required=True, widget=None, label=None): """ regex can be either a string or a compiled regular expression object. error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ - Field.__init__(self, required, widget) + Field.__init__(self, required, widget, label) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex - self.error_message = error_message or u'Enter a valid value.' + self.error_message = error_message or gettext(u'Enter a valid value.') def clean(self, value): """ @@ -163,6 +219,8 @@ 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 not self.regex.search(value): raise ValidationError(self.error_message) return value @@ -173,8 +231,8 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): - def __init__(self, required=True, widget=None): - RegexField.__init__(self, email_re, u'Enter a valid e-mail address.', required, widget) + def __init__(self, required=True, widget=None, label=None): + RegexField.__init__(self, email_re, gettext(u'Enter a valid e-mail address.'), required, widget, label) url_re = re.compile( r'^https?://' # http:// or https:// @@ -190,9 +248,9 @@ except ImportError: URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' class URLField(RegexField): - def __init__(self, required=True, verify_exists=False, widget=None, + def __init__(self, required=True, verify_exists=False, widget=None, label=None, validator_user_agent=URL_VALIDATOR_USER_AGENT): - RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget) + RegexField.__init__(self, url_re, gettext(u'Enter a valid URL.'), required, widget, label) self.verify_exists = verify_exists self.user_agent = validator_user_agent @@ -212,9 +270,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): @@ -226,10 +284,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): @@ -239,37 +297,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 2730bfe4e4..96948264e4 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -2,9 +2,11 @@ Form classes """ +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 NON_FIELD_ERRORS = '__all__' @@ -13,23 +15,37 @@ def pretty_name(name): name = name[0].upper() + name[1:] return name.replace('_', ' ') +class SortedDictFromList(SortedDict): + "A dictionary that keeps its keys in the order in which they're inserted." + # This is different than django.utils.datastructures.SortedDict, because + # this takes a list/tuple as the argument to __init__(). + def __init__(self, data=None): + if data is None: data = [] + self.keyOrder = [d[0] for d in data] + dict.__init__(self, dict(data)) + class DeclarativeFieldsMetaclass(type): "Metaclass that converts Field attributes to a dictionary called 'fields'." def __new__(cls, name, bases, attrs): - attrs['fields'] = dict([(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)]) + fields = [(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)] + fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) + 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): @@ -44,60 +60,75 @@ 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 bool(self.errors()) + 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 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) + output.append(normal_row % {'errors': bf_errors, 'label': bf.label_tag(escape(bf.label+':')), 'field': bf}) + if top_errors: + output.insert(0, error_row % top_errors) + if hidden_fields: # Insert any hidden fields in the last row. + str_hidden = u''.join(hidden_fields) + if output: + last_row = output[-1] + # Chop off the trailing row_ender (e.g. '') and insert the hidden fields. + output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender + else: # If there aren't any rows in the output, just append the hidden fields. + output.append(str_hidden) + return u'\n'.join(output) def as_table(self): "Returns this form rendered as HTML s -- excluding the
." - return u'\n'.join(['%s:%s' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'%(label)s%(field)s', u'%s', '', True) def as_ul(self): "Returns this form rendered as HTML
  • s -- excluding the
      ." - return u'\n'.join(['
    • %s: %s
    • ' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'
    • %(errors)s%(label)s %(field)s
    • ', u'
    • %s
    • ', '', False) - def as_table_with_errors(self): - "Returns this form rendered as HTML 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('
        %s
      ' % '\n'.join(['
    • %s
    • ' % 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('
        %s
      ' % '\n'.join(['
    • %s
    • ' % e for e in bf.errors])) - output.append('%s:%s' % (pretty_name(name), bf)) - return u'\n'.join(output) + def as_p(self): + "Returns this form rendered as HTML

      s." + return self._html_output(u'

      %(label)s %(field)s

      ', u'

      %s

      ', '

      ', True) - def as_ul_with_errors(self): - "Returns this form rendered as HTML
    • 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('
      • %s
    • ' % '\n'.join(['
    • %s
    • ' % e for e in self.errors()[NON_FIELD_ERRORS]])) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - line = '
    • ' - if bf.errors: - line += '
        %s
      ' % '\n'.join(['
    • %s
    • ' % e for e in bf.errors]) - line += '%s: %s' % (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): """ @@ -105,8 +136,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 @@ -126,40 +163,56 @@ 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) + self.label = self.field.label or pretty_name(name) - def __str__(self): + def __unicode__(self): "Renders this field as an HTML widget." # Use the 'widget' attribute on the field to determine which type # of HTML widget to use. - 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): """ @@ -171,15 +224,46 @@ class BoundField(object): "Returns a string of HTML for representing this as a ' % (flatatt(final_attrs), escape(value)) class CheckboxInput(Widget): + def __init__(self, attrs=None, check_test=bool): + # check_test is a callable that takes a value and returns True + # if the checkbox should be checked for that value. + self.attrs = attrs or {} + self.check_test = check_test + def render(self, name, value, attrs=None): final_attrs = self.build_attrs(attrs, type='checkbox', name=name) - if value: final_attrs['checked'] = 'checked' + try: + result = self.check_test(value) + except: # Silently catch exceptions + result = False + if result: + final_attrs['checked'] = 'checked' + if value not in ('', True, False, None): + final_attrs['value'] = smart_unicode(value) # Only add the 'value' attribute if a value is non-empty. return u'' % flatatt(final_attrs) class Select(Widget): @@ -109,36 +148,44 @@ class SelectMultiple(Widget): output.append(u'') return u'\n'.join(output) -class RadioInput(object): - "An object used by RadioFieldRenderer that represents a single ." - def __init__(self, name, value, attrs, choice): - self.name, self.value = name, value - self.attrs = attrs or {} - self.choice_value, self.choice_label = choice + def value_from_datadict(self, data, name): + if isinstance(data, MultiValueDict): + return data.getlist(name) + return data.get(name, None) - def __str__(self): +class RadioInput(StrAndUnicode): + "An object used by RadioFieldRenderer that represents a single ." + def __init__(self, name, value, attrs, choice, index): + self.name, self.value = name, value + self.attrs = attrs + self.choice_value, self.choice_label = choice + self.index = index + + def __unicode__(self): return u'' % (self.tag(), self.choice_label) def is_checked(self): return self.value == smart_unicode(self.choice_value) def tag(self): + if self.attrs.has_key('id'): + self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value) if self.is_checked(): final_attrs['checked'] = 'checked' return u'' % flatatt(final_attrs) -class RadioFieldRenderer(object): +class RadioFieldRenderer(StrAndUnicode): "An object used by RadioSelect to enable customization of radio widgets." def __init__(self, name, value, attrs, choices): self.name, self.value, self.attrs = name, value, attrs self.choices = choices def __iter__(self): - for choice in self.choices: - yield RadioInput(self.name, self.value, self.attrs, choice) + for i, choice in enumerate(self.choices): + yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i) - def __str__(self): + def __unicode__(self): "Outputs a
        for this set of radio fields." return u'
          \n%s\n
        ' % u'\n'.join([u'
      • %s
      • ' % w for w in self]) @@ -147,7 +194,36 @@ class RadioSelect(Select): "Returns a RadioFieldRenderer instance rather than a Unicode string." if value is None: value = '' str_value = smart_unicode(value) # Normalize to string. + attrs = attrs or {} return RadioFieldRenderer(name, str_value, attrs, list(chain(self.choices, choices))) -class CheckboxSelectMultiple(Widget): - pass + def id_for_label(self, id_): + # RadioSelect is represented by multiple fields, + # each of which has a distinct ID. The IDs are made distinct by a "_X" + # suffix, where X is the zero-based index of the radio field. Thus, + # the label for a RadioSelect should reference the first one ('_0'). + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) + +class CheckboxSelectMultiple(SelectMultiple): + def render(self, name, value, attrs=None, choices=()): + if value is None: value = [] + final_attrs = self.build_attrs(attrs, name=name) + output = [u'
          '] + str_values = set([smart_unicode(v) for v in value]) # Normalize to strings. + cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values) + for option_value, option_label in chain(self.choices, choices): + option_value = smart_unicode(option_value) + rendered_cb = cb.render(name, option_value) + output.append(u'
        • ' % (rendered_cb, escape(smart_unicode(option_label)))) + output.append(u'
        ') + return u'\n'.join(output) + + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py new file mode 100644 index 0000000000..0b9ac05edb --- /dev/null +++ b/django/oldforms/__init__.py @@ -0,0 +1,1008 @@ +from django.core import validators +from django.core.exceptions import PermissionDenied +from django.utils.html import escape +from django.conf import settings +from django.utils.translation import gettext, ngettext + +FORM_FIELD_ID_PREFIX = 'id_' + +class EmptyValue(Exception): + "This is raised when empty data is provided" + pass + +class Manipulator(object): + # List of permission strings. User must have at least one to manipulate. + # None means everybody has permission. + required_permission = '' + + def __init__(self): + # List of FormField objects + self.fields = [] + + def __getitem__(self, field_name): + "Looks up field by field name; raises KeyError on failure" + for field in self.fields: + if field.field_name == field_name: + return field + raise KeyError, "Field %s not found\n%s" % (field_name, repr(self.fields)) + + def __delitem__(self, field_name): + "Deletes the field with the given field name; raises KeyError on failure" + for i, field in enumerate(self.fields): + if field.field_name == field_name: + del self.fields[i] + return + raise KeyError, "Field %s not found" % field_name + + def check_permissions(self, user): + """Confirms user has required permissions to use this manipulator; raises + PermissionDenied on failure.""" + if self.required_permission is None: + return + if user.has_perm(self.required_permission): + return + raise PermissionDenied + + def prepare(self, new_data): + """ + Makes any necessary preparations to new_data, in place, before data has + been validated. + """ + for field in self.fields: + field.prepare(new_data) + + def get_validation_errors(self, new_data): + "Returns dictionary mapping field_names to error-message lists" + errors = {} + self.prepare(new_data) + for field in self.fields: + errors.update(field.get_validation_errors(new_data)) + val_name = 'validate_%s' % field.field_name + if hasattr(self, val_name): + val = getattr(self, val_name) + try: + field.run_validator(new_data, val) + except (validators.ValidationError, validators.CriticalValidationError), e: + errors.setdefault(field.field_name, []).extend(e.messages) + +# if field.is_required and not new_data.get(field.field_name, False): +# errors.setdefault(field.field_name, []).append(gettext_lazy('This field is required.')) +# continue +# try: +# validator_list = field.validator_list +# if hasattr(self, 'validate_%s' % field.field_name): +# validator_list.append(getattr(self, 'validate_%s' % field.field_name)) +# for validator in validator_list: +# if field.is_required or new_data.get(field.field_name, False) or hasattr(validator, 'always_test'): +# try: +# if hasattr(field, 'requires_data_list'): +# validator(new_data.getlist(field.field_name), new_data) +# else: +# validator(new_data.get(field.field_name, ''), new_data) +# except validators.ValidationError, e: +# errors.setdefault(field.field_name, []).extend(e.messages) +# # If a CriticalValidationError is raised, ignore any other ValidationErrors +# # for this particular field +# except validators.CriticalValidationError, e: +# errors.setdefault(field.field_name, []).extend(e.messages) + return errors + + def save(self, new_data): + "Saves the changes and returns the new object" + # changes is a dictionary-like object keyed by field_name + raise NotImplementedError + + def do_html2python(self, new_data): + """ + Convert the data from HTML data types to Python datatypes, changing the + object in place. This happens after validation but before storage. This + must happen after validation because html2python functions aren't + expected to deal with invalid input. + """ + for field in self.fields: + field.convert_post_data(new_data) + +class FormWrapper(object): + """ + A wrapper linking a Manipulator to the template system. + This allows dictionary-style lookups of formfields. It also handles feeding + prepopulated data and validation error messages to the formfield objects. + """ + def __init__(self, manipulator, data=None, error_dict=None, edit_inline=True): + self.manipulator = manipulator + if data is None: + data = {} + if error_dict is None: + error_dict = {} + self.data = data + self.error_dict = error_dict + self._inline_collections = None + self.edit_inline = edit_inline + + def __repr__(self): + return repr(self.__dict__) + + def __getitem__(self, key): + for field in self.manipulator.fields: + if field.field_name == key: + data = field.extract_data(self.data) + return FormFieldWrapper(field, data, self.error_dict.get(field.field_name, [])) + if self.edit_inline: + self.fill_inline_collections() + for inline_collection in self._inline_collections: + if inline_collection.name == key: + return inline_collection + raise KeyError, "Could not find Formfield or InlineObjectCollection named %r" % key + + def fill_inline_collections(self): + if not self._inline_collections: + ic = [] + related_objects = self.manipulator.get_related_objects() + for rel_obj in related_objects: + data = rel_obj.extract_data(self.data) + inline_collection = InlineObjectCollection(self.manipulator, rel_obj, data, self.error_dict) + ic.append(inline_collection) + self._inline_collections = ic + + def has_errors(self): + return self.error_dict != {} + + def _get_fields(self): + try: + return self._fields + except AttributeError: + self._fields = [self.__getitem__(field.field_name) for field in self.manipulator.fields] + return self._fields + + fields = property(_get_fields) + +class FormFieldWrapper(object): + "A bridge between the template system and an individual form field. Used by FormWrapper." + def __init__(self, formfield, data, error_list): + self.formfield, self.data, self.error_list = formfield, data, error_list + self.field_name = self.formfield.field_name # for convenience in templates + + def __str__(self): + "Renders the field" + return str(self.formfield.render(self.data)) + + def __repr__(self): + return '' % self.formfield.field_name + + def field_list(self): + """ + Like __str__(), but returns a list. Use this when the field's render() + method returns a list. + """ + return self.formfield.render(self.data) + + def errors(self): + return self.error_list + + def html_error_list(self): + if self.errors(): + return '
        • %s
        ' % '
      • '.join([escape(e) for e in self.errors()]) + else: + return '' + + def get_id(self): + return self.formfield.get_id() + +class FormFieldCollection(FormFieldWrapper): + "A utility class that gives the template access to a dict of FormFieldWrappers" + def __init__(self, formfield_dict): + self.formfield_dict = formfield_dict + + def __str__(self): + return str(self.formfield_dict) + + def __getitem__(self, template_key): + "Look up field by template key; raise KeyError on failure" + return self.formfield_dict[template_key] + + def __repr__(self): + return "" % self.formfield_dict + + def errors(self): + "Returns list of all errors in this collection's formfields" + errors = [] + for field in self.formfield_dict.values(): + if hasattr(field, 'errors'): + errors.extend(field.errors()) + return errors + + def has_errors(self): + return bool(len(self.errors())) + + def html_combined_error_list(self): + return ''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')]) + +class InlineObjectCollection(object): + "An object that acts like a sparse list of form field collections." + def __init__(self, parent_manipulator, rel_obj, data, errors): + self.parent_manipulator = parent_manipulator + self.rel_obj = rel_obj + self.data = data + self.errors = errors + self._collections = None + self.name = rel_obj.name + + def __len__(self): + self.fill() + return self._collections.__len__() + + def __getitem__(self, k): + self.fill() + return self._collections.__getitem__(k) + + def __setitem__(self, k, v): + self.fill() + return self._collections.__setitem__(k,v) + + def __delitem__(self, k): + self.fill() + return self._collections.__delitem__(k) + + def __iter__(self): + self.fill() + return iter(self._collections.values()) + + def items(self): + self.fill() + return self._collections.items() + + def fill(self): + if self._collections: + return + else: + var_name = self.rel_obj.opts.object_name.lower() + collections = {} + orig = None + if hasattr(self.parent_manipulator, 'original_object'): + orig = self.parent_manipulator.original_object + orig_list = self.rel_obj.get_list(orig) + + for i, instance in enumerate(orig_list): + collection = {'original': instance} + for f in self.rel_obj.editable_fields(): + for field_name in f.get_manipulator_field_names(''): + full_field_name = '%s.%d.%s' % (var_name, i, field_name) + field = self.parent_manipulator[full_field_name] + data = field.extract_data(self.data) + errors = self.errors.get(full_field_name, []) + collection[field_name] = FormFieldWrapper(field, data, errors) + collections[i] = FormFieldCollection(collection) + self._collections = collections + + +class FormField(object): + """Abstract class representing a form field. + + Classes that extend FormField should define the following attributes: + field_name + The field's name for use by programs. + validator_list + A list of validation tests (callback functions) that the data for + this field must pass in order to be added or changed. + is_required + A Boolean. Is it a required field? + Subclasses should also implement a render(data) method, which is responsible + for rending the form field in XHTML. + """ + def __str__(self): + return self.render('') + + def __repr__(self): + return 'FormField "%s"' % self.field_name + + def prepare(self, new_data): + "Hook for doing something to new_data (in place) before validation." + pass + + def html2python(data): + "Hook for converting an HTML datatype (e.g. 'on' for checkboxes) to a Python type" + return data + html2python = staticmethod(html2python) + + def render(self, data): + raise NotImplementedError + + def get_member_name(self): + if hasattr(self, 'member_name'): + return self.member_name + else: + return self.field_name + + def extract_data(self, data_dict): + if hasattr(self, 'requires_data_list') and hasattr(data_dict, 'getlist'): + data = data_dict.getlist(self.get_member_name()) + else: + data = data_dict.get(self.get_member_name(), None) + if data is None: + data = '' + return data + + def convert_post_data(self, new_data): + name = self.get_member_name() + if new_data.has_key(self.field_name): + d = new_data.getlist(self.field_name) + try: + converted_data = [self.__class__.html2python(data) for data in d] + except ValueError: + converted_data = d + new_data.setlist(name, converted_data) + else: + try: + #individual fields deal with None values themselves + new_data.setlist(name, [self.__class__.html2python(None)]) + except EmptyValue: + new_data.setlist(name, []) + + + def run_validator(self, new_data, validator): + if self.is_required or new_data.get(self.field_name, False) or hasattr(validator, 'always_test'): + if hasattr(self, 'requires_data_list'): + validator(new_data.getlist(self.field_name), new_data) + else: + validator(new_data.get(self.field_name, ''), new_data) + + def get_validation_errors(self, new_data): + errors = {} + if self.is_required and not new_data.get(self.field_name, False): + errors.setdefault(self.field_name, []).append(gettext('This field is required.')) + return errors + try: + for validator in self.validator_list: + try: + self.run_validator(new_data, validator) + except validators.ValidationError, e: + errors.setdefault(self.field_name, []).extend(e.messages) + # If a CriticalValidationError is raised, ignore any other ValidationErrors + # for this particular field + except validators.CriticalValidationError, e: + errors.setdefault(self.field_name, []).extend(e.messages) + return errors + + def get_id(self): + "Returns the HTML 'id' attribute for this form field." + return FORM_FIELD_ID_PREFIX + self.field_name + +#################### +# GENERIC WIDGETS # +#################### + +class TextField(FormField): + input_type = "text" + def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.length, self.maxlength = length, maxlength + self.is_required = is_required + self.validator_list = [self.isValidLength, self.hasNoNewlines] + validator_list + if member_name != None: + self.member_name = member_name + + def isValidLength(self, data, form): + if data and self.maxlength and len(data.decode(settings.DEFAULT_CHARSET)) > self.maxlength: + raise validators.ValidationError, ngettext("Ensure your text is less than %s character.", + "Ensure your text is less than %s characters.", self.maxlength) % self.maxlength + + def hasNoNewlines(self, data, form): + if data and '\n' in data: + raise validators.ValidationError, gettext("Line breaks are not allowed here.") + + def render(self, data): + if data is None: + data = '' + maxlength = '' + if self.maxlength: + maxlength = 'maxlength="%s" ' % self.maxlength + if isinstance(data, unicode): + data = data.encode(settings.DEFAULT_CHARSET) + return '' % \ + (self.input_type, self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', + self.field_name, self.length, escape(data), maxlength) + + def html2python(data): + return data + html2python = staticmethod(html2python) + +class PasswordField(TextField): + input_type = "password" + +class LargeTextField(TextField): + def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=None, maxlength=None): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.rows, self.cols, self.is_required = rows, cols, is_required + self.validator_list = validator_list[:] + if maxlength: + self.validator_list.append(self.isValidLength) + self.maxlength = maxlength + + def render(self, data): + if data is None: + data = '' + if isinstance(data, unicode): + data = data.encode(settings.DEFAULT_CHARSET) + return '' % \ + (self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', + self.field_name, self.rows, self.cols, escape(data)) + +class HiddenField(FormField): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.field_name, self.is_required = field_name, is_required + self.validator_list = validator_list[:] + + def render(self, data): + return '' % \ + (self.get_id(), self.field_name, escape(data)) + +class CheckboxField(FormField): + def __init__(self, field_name, checked_by_default=False, validator_list=None, is_required=False): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.checked_by_default = checked_by_default + self.is_required = is_required + self.validator_list = validator_list[:] + + def render(self, data): + checked_html = '' + if data or (data is '' and self.checked_by_default): + checked_html = ' checked="checked"' + return '' % \ + (self.get_id(), self.__class__.__name__, + self.field_name, checked_html) + + def html2python(data): + "Convert value from browser ('on' or '') to a Python boolean" + if data == 'on': + return True + return False + html2python = staticmethod(html2python) + +class SelectField(FormField): + def __init__(self, field_name, choices=None, size=1, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + if choices is None: choices = [] + self.field_name = field_name + # choices is a list of (value, human-readable key) tuples because order matters + self.choices, self.size, self.is_required = choices, size, is_required + self.validator_list = [self.isValidChoice] + validator_list + if member_name != None: + self.member_name = member_name + + def render(self, data): + output = ['') + return '\n'.join(output) + + def isValidChoice(self, data, form): + str_data = str(data) + str_choices = [str(item[0]) for item in self.choices] + if str_data not in str_choices: + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data': str_data, 'choices': str_choices} + +class NullSelectField(SelectField): + "This SelectField converts blank fields to None" + def html2python(data): + if not data: + return None + return data + html2python = staticmethod(html2python) + +class RadioSelectField(FormField): + def __init__(self, field_name, choices=None, ul_class='', is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + if choices is None: choices = [] + self.field_name = field_name + # choices is a list of (value, human-readable key) tuples because order matters + self.choices, self.is_required = choices, is_required + self.validator_list = [self.isValidChoice] + validator_list + self.ul_class = ul_class + if member_name != None: + self.member_name = member_name + + def render(self, data): + """ + Returns a special object, RadioFieldRenderer, that is iterable *and* + has a default str() rendered output. + + This allows for flexible use in templates. You can just use the default + rendering: + + {{ field_name }} + + ...which will output the radio buttons in an unordered list. + Or, you can manually traverse each radio option for special layout: + + {% for option in field_name.field_list %} + {{ option.field }} {{ option.label }}
        + {% endfor %} + """ + class RadioFieldRenderer: + def __init__(self, datalist, ul_class): + self.datalist, self.ul_class = datalist, ul_class + def __str__(self): + "Default str() output for this radio field -- a
          " + output = ['' % (self.ul_class and ' class="%s"' % self.ul_class or '')] + output.extend(['
        • %s %s
        • ' % (d['field'], d['label']) for d in self.datalist]) + output.append('
        ') + return ''.join(output) + def __iter__(self): + for d in self.datalist: + yield d + def __len__(self): + return len(self.datalist) + datalist = [] + str_data = str(data) # normalize to string + for i, (value, display_name) in enumerate(self.choices): + selected_html = '' + if str(value) == str_data: + selected_html = ' checked="checked"' + datalist.append({ + 'value': value, + 'name': display_name, + 'field': '' % \ + (self.get_id() + '_' + str(i), self.field_name, value, selected_html), + 'label': '' % \ + (self.get_id() + '_' + str(i), display_name), + }) + return RadioFieldRenderer(datalist, self.ul_class) + + def isValidChoice(self, data, form): + str_data = str(data) + str_choices = [str(item[0]) for item in self.choices] + if str_data not in str_choices: + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':str_data, 'choices':str_choices} + +class NullBooleanField(SelectField): + "This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + SelectField.__init__(self, field_name, choices=[('1', 'Unknown'), ('2', 'Yes'), ('3', 'No')], + is_required=is_required, validator_list=validator_list) + + def render(self, data): + if data is None: data = '1' + elif data == True: data = '2' + elif data == False: data = '3' + return SelectField.render(self, data) + + def html2python(data): + return {None: None, '1': None, '2': True, '3': False}[data] + html2python = staticmethod(html2python) + +class SelectMultipleField(SelectField): + requires_data_list = True + def render(self, data): + output = ['') + return '\n'.join(output) + + def isValidChoice(self, field_data, all_data): + # data is something like ['1', '2', '3'] + str_choices = [str(item[0]) for item in self.choices] + for val in map(str, field_data): + if val not in str_choices: + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':val, 'choices':str_choices} + + def html2python(data): + if data is None: + raise EmptyValue + return data + html2python = staticmethod(html2python) + +class CheckboxSelectMultipleField(SelectMultipleField): + """ + This has an identical interface to SelectMultipleField, except the rendered + widget is different. Instead of a es. + + Of course, that results in multiple form elements for the same "single" + field, so this class's prepare() method flattens the split data elements + back into the single list that validators, renderers and save() expect. + """ + requires_data_list = True + def __init__(self, field_name, choices=None, ul_class='', validator_list=None): + if validator_list is None: validator_list = [] + if choices is None: choices = [] + self.ul_class = ul_class + SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list) + + def prepare(self, new_data): + # new_data has "split" this field into several fields, so flatten it + # back into a single list. + data_list = [] + for value, readable_value in self.choices: + if new_data.get('%s%s' % (self.field_name, value), '') == 'on': + data_list.append(value) + new_data.setlist(self.field_name, data_list) + + def render(self, data): + output = ['' % (self.ul_class and ' class="%s"' % self.ul_class or '')] + str_data_list = map(str, data) # normalize to strings + for value, choice in self.choices: + checked_html = '' + if str(value) in str_data_list: + checked_html = ' checked="checked"' + field_name = '%s%s' % (self.field_name, value) + output.append('
      • ' % \ + (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, + self.get_id() + escape(value), choice)) + output.append('
      ') + return '\n'.join(output) + +#################### +# FILE UPLOADS # +#################### + +class FileUploadField(FormField): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.field_name, self.is_required = field_name, is_required + self.validator_list = [self.isNonEmptyFile] + validator_list + + def isNonEmptyFile(self, field_data, all_data): + try: + content = field_data['content'] + except TypeError: + raise validators.CriticalValidationError, gettext("No file was submitted. Check the encoding type on the form.") + if not content: + raise validators.CriticalValidationError, gettext("The submitted file is empty.") + + def render(self, data): + return '' % \ + (self.get_id(), self.__class__.__name__, self.field_name) + + def html2python(data): + if data is None: + raise EmptyValue + return data + html2python = staticmethod(html2python) + +class ImageUploadField(FileUploadField): + "A FileUploadField that raises CriticalValidationError if the uploaded file isn't an image." + def __init__(self, *args, **kwargs): + FileUploadField.__init__(self, *args, **kwargs) + self.validator_list.insert(0, self.isValidImage) + + def isValidImage(self, field_data, all_data): + try: + validators.isValidImage(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +#################### +# INTEGERS/FLOATS # +#################### + +class IntegerField(TextField): + def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] + validator_list = [self.isInteger] + validator_list + if member_name is not None: + self.member_name = member_name + TextField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isInteger(self, field_data, all_data): + try: + validators.isInteger(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + if data == '' or data is None: + return None + return int(data) + html2python = staticmethod(html2python) + +class SmallIntegerField(IntegerField): + def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isSmallInteger] + validator_list + IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isSmallInteger(self, field_data, all_data): + if not -32768 <= int(field_data) <= 32767: + raise validators.CriticalValidationError, gettext("Enter a whole number between -32,768 and 32,767.") + +class PositiveIntegerField(IntegerField): + def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isPositive] + validator_list + IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isPositive(self, field_data, all_data): + if int(field_data) < 0: + raise validators.CriticalValidationError, gettext("Enter a positive number.") + +class PositiveSmallIntegerField(IntegerField): + def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isPositiveSmall] + validator_list + IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) + + def isPositiveSmall(self, field_data, all_data): + if not 0 <= int(field_data) <= 32767: + raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.") + +class FloatField(TextField): + def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.max_digits, self.decimal_places = max_digits, decimal_places + validator_list = [self.isValidFloat] + validator_list + TextField.__init__(self, field_name, max_digits+2, max_digits+2, is_required, validator_list) + + def isValidFloat(self, field_data, all_data): + v = validators.IsValidFloat(self.max_digits, self.decimal_places) + try: + v(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + if data == '' or data is None: + return None + return float(data) + html2python = staticmethod(html2python) + +#################### +# DATES AND TIMES # +#################### + +class DatetimeField(TextField): + """A FormField that automatically converts its data to a datetime.datetime object. + The data should be in the format YYYY-MM-DD HH:MM:SS.""" + def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + self.field_name = field_name + self.length, self.maxlength = length, maxlength + self.is_required = is_required + self.validator_list = [validators.isValidANSIDatetime] + validator_list + + def html2python(data): + "Converts the field into a datetime.datetime object" + import datetime + try: + date, time = data.split() + y, m, d = date.split('-') + timebits = time.split(':') + h, mn = timebits[:2] + if len(timebits) > 2: + s = int(timebits[2]) + else: + s = 0 + return datetime.datetime(int(y), int(m), int(d), int(h), int(mn), s) + except ValueError: + return None + html2python = staticmethod(html2python) + +class DateField(TextField): + """A FormField that automatically converts its data to a datetime.date object. + The data should be in the format YYYY-MM-DD.""" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidDate] + validator_list + TextField.__init__(self, field_name, length=10, maxlength=10, + is_required=is_required, validator_list=validator_list) + + def isValidDate(self, field_data, all_data): + try: + validators.isValidANSIDate(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + "Converts the field into a datetime.date object" + import time, datetime + try: + time_tuple = time.strptime(data, '%Y-%m-%d') + return datetime.date(*time_tuple[0:3]) + except (ValueError, TypeError): + return None + html2python = staticmethod(html2python) + +class TimeField(TextField): + """A FormField that automatically converts its data to a datetime.time object. + The data should be in the format HH:MM:SS or HH:MM:SS.mmmmmm.""" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidTime] + validator_list + TextField.__init__(self, field_name, length=8, maxlength=8, + is_required=is_required, validator_list=validator_list) + + def isValidTime(self, field_data, all_data): + try: + validators.isValidANSITime(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + "Converts the field into a datetime.time object" + import time, datetime + try: + part_list = data.split('.') + try: + time_tuple = time.strptime(part_list[0], '%H:%M:%S') + except ValueError: # seconds weren't provided + time_tuple = time.strptime(part_list[0], '%H:%M') + t = datetime.time(*time_tuple[3:6]) + if (len(part_list) == 2): + t = t.replace(microsecond=int(part_list[1])) + return t + except (ValueError, TypeError, AttributeError): + return None + html2python = staticmethod(html2python) + +#################### +# INTERNET-RELATED # +#################### + +class EmailField(TextField): + "A convenience FormField for validating e-mail addresses" + def __init__(self, field_name, length=50, maxlength=75, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidEmail] + validator_list + TextField.__init__(self, field_name, length, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isValidEmail(self, field_data, all_data): + try: + validators.isValidEmail(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +class URLField(TextField): + "A convenience FormField for validating URLs" + def __init__(self, field_name, length=50, maxlength=200, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidURL] + validator_list + TextField.__init__(self, field_name, length=length, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isValidURL(self, field_data, all_data): + try: + validators.isValidURL(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +class IPAddressField(TextField): + def __init__(self, field_name, length=15, maxlength=15, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidIPAddress] + validator_list + TextField.__init__(self, field_name, length=length, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isValidIPAddress(self, field_data, all_data): + try: + validators.isValidIPAddress4(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + return data or None + html2python = staticmethod(html2python) + +#################### +# MISCELLANEOUS # +#################### + +class FilePathField(SelectField): + "A SelectField whose choices are the files in a given directory." + def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None): + import os + from django.db.models import BLANK_CHOICE_DASH + if match is not None: + import re + match_re = re.compile(match) + choices = not is_required and BLANK_CHOICE_DASH[:] or [] + if recursive: + for root, dirs, files in os.walk(path): + for f in files: + if match is None or match_re.search(f): + choices.append((os.path.join(root, f), f)) + else: + try: + for f in os.listdir(path): + full_file = os.path.join(path, f) + if os.path.isfile(full_file) and (match is None or match_re.search(f)): + choices.append((full_file, f)) + except OSError: + pass + SelectField.__init__(self, field_name, choices, 1, is_required, validator_list) + +class PhoneNumberField(TextField): + "A convenience FormField for validating phone numbers (e.g. '630-555-1234')" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidPhone] + validator_list + TextField.__init__(self, field_name, length=12, maxlength=12, + is_required=is_required, validator_list=validator_list) + + def isValidPhone(self, field_data, all_data): + try: + validators.isValidPhone(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + +class USStateField(TextField): + "A convenience FormField for validating U.S. states (e.g. 'IL')" + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isValidUSState] + validator_list + TextField.__init__(self, field_name, length=2, maxlength=2, + is_required=is_required, validator_list=validator_list) + + def isValidUSState(self, field_data, all_data): + try: + validators.isValidUSState(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def html2python(data): + return data.upper() # Should always be stored in upper case + html2python = staticmethod(html2python) + +class CommaSeparatedIntegerField(TextField): + "A convenience FormField for validating comma-separated integer fields" + def __init__(self, field_name, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] + validator_list = [self.isCommaSeparatedIntegerList] + validator_list + TextField.__init__(self, field_name, length=20, maxlength=maxlength, + is_required=is_required, validator_list=validator_list) + + def isCommaSeparatedIntegerList(self, field_data, all_data): + try: + validators.isCommaSeparatedIntegerList(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages + + def render(self, data): + if data is None: + data = '' + elif isinstance(data, (list, tuple)): + data = ','.join(data) + return super(CommaSeparatedIntegerField, self).render(data) + +class RawIdAdminField(CommaSeparatedIntegerField): + def html2python(data): + if data: + return data.split(',') + else: + return [] + html2python = staticmethod(html2python) + +class XMLLargeTextField(LargeTextField): + """ + A LargeTextField with an XML validator. The schema_path argument is the + full path to a Relax NG compact schema to validate against. + """ + def __init__(self, field_name, schema_path, **kwargs): + self.schema_path = schema_path + kwargs.setdefault('validator_list', []).insert(0, self.isValidXML) + LargeTextField.__init__(self, field_name, **kwargs) + + def isValidXML(self, field_data, all_data): + v = validators.RelaxNGCompact(self.schema_path) + try: + v(field_data, all_data) + except validators.ValidationError, e: + raise validators.CriticalValidationError, e.messages diff --git a/django/template/__init__.py b/django/template/__init__.py index 5affafeba9..7718801684 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -742,7 +742,11 @@ class VariableNode(Node): def encode_output(self, output): # Check type so that we don't run str() on a Unicode object if not isinstance(output, basestring): - return str(output) + try: + return str(output) + except UnicodeEncodeError: + # If __str__() returns a Unicode object, convert it to bytestring. + return unicode(output).encode(settings.DEFAULT_CHARSET) elif isinstance(output, unicode): return output.encode(settings.DEFAULT_CHARSET) else: diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py index 3a03fa59e4..28987f7544 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -1,6 +1,6 @@ from django.core.xheaders import populate_xheaders from django.template import loader -from django import forms +from django import oldforms from django.db.models import FileField from django.contrib.auth.views import redirect_to_login from django.template import RequestContext @@ -56,7 +56,7 @@ def create_object(request, model, template_name=None, new_data = manipulator.flatten_data() # Create the FormWrapper, template, context, response - form = forms.FormWrapper(manipulator, new_data, errors) + form = oldforms.FormWrapper(manipulator, new_data, errors) if not template_name: template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) @@ -128,7 +128,7 @@ def update_object(request, model, object_id=None, slug=None, # This makes sure the form acurate represents the fields of the place. new_data = manipulator.flatten_data() - form = forms.FormWrapper(manipulator, new_data, errors) + form = oldforms.FormWrapper(manipulator, new_data, errors) if not template_name: template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index bd0f17c56a..1836ce4a9f 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -84,7 +84,7 @@ def object_detail(request, queryset, object_id=None, slug=None, context_processors=None, template_object_name='object', mimetype=None): """ - Generic list of objects. + Generic detail of an object. Templates: ``/_detail.html`` Context: diff --git a/docs/add_ons.txt b/docs/add_ons.txt index a0377700d7..58c01c4fc0 100644 --- a/docs/add_ons.txt +++ b/docs/add_ons.txt @@ -48,6 +48,23 @@ See the `csrf documentation`_. .. _csrf documentation: http://www.djangoproject.com/documentation/csrf/ +formtools +========= + +**New in Django development version** + +A set of high-level abstractions for Django forms (django.newforms). + +django.contrib.formtools.preview +-------------------------------- + +An abstraction of the following workflow: + +"Display an HTML form, force a preview, then do something with the submission." + +Full documentation for this feature does not yet exist, but you can read the +code and docstrings in ``django/contrib/formtools/preview.py`` for a start. + humanize ======== diff --git a/docs/contributing.txt b/docs/contributing.txt index 6ff0b038a3..de9236f6c1 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -22,6 +22,9 @@ of the community, so there are many ways you can help Django's development: likely to be skeptical of large-scale suggestions without some code to back it up. + * Triage patches that have been submitted by other users. Please read + `Ticket triage`_ below, for details on the triage process. + That's all you need to know if you'd like to join the Django development community. The rest of this document describes the details of how our community works and how it handles bugs, mailing lists, and all the other minutiae of @@ -44,8 +47,10 @@ particular: * **Do** write complete, reproducible, specific bug reports. Include as much information as you possibly can, complete with code snippets, test - cases, etc. A minimal example that illustrates the bug in a nice small - test case is the best possible bug report. + cases, etc. This means including a clear, concise description of the + problem, and a clear set of instructions for replicating the problem. + A minimal example that illustrates the bug in a nice small test case + is the best possible bug report. * **Don't** use the ticket system to ask support questions. Use the `django-users`_ list, or the `#django`_ IRC channel for that. @@ -121,6 +126,50 @@ Patch style it obvious that the ticket includes a patch, and it will add the ticket to the `list of tickets with patches`_. + * The code required to fix a problem or add a feature is an essential part + of a patch, but it is not the only part. A good patch should also include + a regression test to validate the behavior that has been fixed (and prevent + the problem from arising again). + + * If the code associated with a patch adds a new feature, or modifies behavior + of an existing feature, the patch should also contain documentation. + +Non-trivial patches +------------------- + +A "non-trivial" patch is one that is more than a simple bug fix. It's a patch +that introduces Django functionality and makes some sort of design decision. + +If you provide a non-trivial patch, include evidence that alternatives have +been discussed on `django-developers`_. If you're not sure whether your patch +should be considered non-trivial, just ask. + +Ticket triage +============= + +Unfortunately, not all bug reports in the `ticket tracker`_ provide all +the `required details`_. A number of tickets have patches, but those patches +don't meet all the requirements of a `good patch`_. + +One way to help out is to *triage* bugs that have been reported by other users. +Pick an open ticket that is missing some details, and try to replicate the +problem. Fill in the missing pieces of the report. If the ticket doesn't have +a patch, create one. + +Once you've completed all the missing details on the ticket and you have a +patch with all the required features, e-mail `django-developers`_. Indicate +that you have triaged a ticket, and recommend a course of action for dealing +with that ticket. + +At first, this may require you to be persistent. If you find that your triaged +ticket still isn't getting attention, occasional polite requests for eyeballs +to look at your ticket may be necessary. However, as you earn a reputation for +quality triage work, you should find that it is easier to get the developers' +attention. + +.. _required details: `Reporting bugs`_ +.. _good patch: `Patch style`_ + Submitting and maintaining translations ======================================= @@ -338,21 +387,63 @@ trunk more than once. Using branches -------------- -To test a given branch, you can simply check out the entire branch, like so:: +To use a branch, you'll need to do two things: + + * Get the branch's code through Subversion. + + * Point your Python ``site-packages`` directory at the branch's version of + the ``django`` package rather than the version you already have + installed. + +Getting the code from Subversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the latest version of a branch's code, check it out using Subversion:: svn co http://code.djangoproject.com/svn/django/branches// -Or, if you've got a working directory you'd like to switch to use a branch, -you can use:: +...where ```` is the branch's name. See the `list of branch names`_. + +Alternatively, you can automatically convert an existing directory of the +Django source code as long as you've checked it out via Subversion. To do the +conversion, execute this command from within your ``django`` directory:: svn switch http://code.djangoproject.com/svn/django/branches// -...in the root of your Django sandbox (the directory that contains ``django``, -``docs``, and ``tests``). - The advantage of using ``svn switch`` instead of ``svn co`` is that the ``switch`` command retains any changes you might have made to your local copy -of the code. It attempts to merge those changes into the "switched" code. +of the code. It attempts to merge those changes into the "switched" code. The +disadvantage is that it may cause conflicts with your local changes if the +"switched" code has altered the same lines of code. + +(Note that if you use ``svn switch``, you don't need to point Python at the new +version, as explained in the next section.) + +.. _list of branch names: http://code.djangoproject.com/browser/django/branches + +Pointing Python at the new Django version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you've retrieved the branch's code, you'll need to change your Python +``site-packages`` directory so that it points to the branch version of the +``django`` directory. (The ``site-packages`` directory is somewhere such as +``/usr/lib/python2.4/site-packages`` or +``/usr/local/lib/python2.4/site-packages`` or ``C:\Python\site-packages``.) + +The simplest way to do this is by renaming the old ``django`` directory to +``django.OLD`` and moving the trunk version of the code into the directory +and calling it ``django``. + +Alternatively, you can use a symlink called ``django`` that points to the +location of the branch's ``django`` package. If you want to switch back, just +change the symlink to point to the old code. + +If you're using Django 0.95 or earlier and installed it using +``python setup.py install``, you'll have a directory called something like +``Django-0.95-py2.4.egg`` instead of ``django``. In this case, edit the file +``setuptools.pth`` and remove the line that references the Django ``.egg`` +file. Then copy the branch's version of the ``django`` directory into +``site-packages``. Official releases ================= diff --git a/docs/forms.txt b/docs/forms.txt index ff192a4717..6efa24d28f 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -2,15 +2,27 @@ Forms, fields, and manipulators =============================== +Forwards-compatibility note +=========================== + +The legacy forms/manipulators system described in this document is going to be +replaced in the next Django release. If you're starting from scratch, we +strongly encourage you not to waste your time learning this. Instead, learn and +use the django.newforms system, which we have begun to document in the +`newforms documentation`_. + +If you have legacy form/manipulator code, read the "Migration plan" section in +that document to understand how we're making the switch. + +.. _newforms documentation: http://www.djangoproject.com/documentation/newforms/ + +Introduction +============ + Once you've got a chance to play with Django's admin interface, you'll probably wonder if the fantastic form validation framework it uses is available to user code. It is, and this document explains how the framework works. - .. admonition:: A note to the lazy - - If all you want to do is present forms for a user to create and/or - update a given object, you may be able to use `generic views`_. - We'll take a top-down approach to examining Django's form validation framework, because much of the time you won't need to use the lower-level APIs. Throughout this document, we'll be working with the following model, a "place" object:: @@ -41,17 +53,17 @@ this document, we'll be working with the following model, a "place" object:: Defining the above class is enough to create an admin interface to a ``Place``, but what if you want to allow public users to submit places? -Manipulators -============ +Automatic Manipulators +====================== The highest-level interface for object creation and modification is the -**Manipulator** framework. A manipulator is a utility class tied to a given -model that "knows" how to create or modify instances of that model and how to -validate data for the object. Manipulators come in two flavors: -``AddManipulators`` and ``ChangeManipulators``. Functionally they are quite -similar, but the former knows how to create new instances of the model, while -the latter modifies existing instances. Both types of classes are automatically -created when you define a new class:: +**automatic Manipulator** framework. An automatic manipulator is a utility +class tied to a given model that "knows" how to create or modify instances of +that model and how to validate data for the object. Automatic Manipulators come +in two flavors: ``AddManipulators`` and ``ChangeManipulators``. Functionally +they are quite similar, but the former knows how to create new instances of the +model, while the latter modifies existing instances. Both types of classes are +automatically created when you define a new class:: >>> from mysite.myapp.models import Place >>> Place.AddManipulator diff --git a/docs/generic_views.txt b/docs/generic_views.txt index 1736770a20..25635f35d8 100644 --- a/docs/generic_views.txt +++ b/docs/generic_views.txt @@ -902,7 +902,7 @@ If ``template_name`` isn't specified, this view will use the template In addition to ``extra_context``, the template's context will be: - * ``form``: A ``django.forms.FormWrapper`` instance representing the form + * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form for editing the object. This lets you refer to form fields easily in the template system. @@ -984,7 +984,7 @@ If ``template_name`` isn't specified, this view will use the template In addition to ``extra_context``, the template's context will be: - * ``form``: A ``django.forms.FormWrapper`` instance representing the form + * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form for editing the object. This lets you refer to form fields easily in the template system. diff --git a/docs/newforms.txt b/docs/newforms.txt new file mode 100644 index 0000000000..c4986eb45e --- /dev/null +++ b/docs/newforms.txt @@ -0,0 +1,300 @@ +==================== +The newforms library +==================== + +``django.newforms`` is a new replacement for ``django.forms``, the old Django +form/manipulator/validation framework. This document explains how to use this +new form library. + +Migration plan +============== + +``django.newforms`` currently is only available in the Django development version +-- i.e., it's not available in the Django 0.95 release. For the next Django +release, our plan is to do the following: + + * As of revision [4208], we've copied the current ``django.forms`` to + ``django.oldforms``. This allows you to upgrade your code *now* rather + than waiting for the backwards-incompatible change and rushing to fix + your code after the fact. Just change your import statements like this:: + + from django import forms # old + from django import oldforms as forms # new + + * At an undecided future date, we will move the current ``django.newforms`` + to ``django.forms``. This will be a backwards-incompatible change, and + anybody who is still using the old version of ``django.forms`` at that + time will need to change their import statements, as described in the + previous bullet. + + * We will remove ``django.oldforms`` in the release *after* the next Django + release -- the release that comes after the release in which we're + creating the new ``django.forms``. + +With this in mind, we recommend you use the following import statement when +using ``django.newforms``:: + + from django import newforms as forms + +This way, your code can refer to the ``forms`` module, and when +``django.newforms`` is renamed to ``django.forms``, you'll only have to change +your ``import`` statements. + +If you prefer "``import *``" syntax, you can do the following:: + + from django.newforms import * + +This will import all fields, widgets, form classes and other various utilities +into your local namespace. Some people find this convenient; others find it +too messy. The choice is yours. + +Overview +======== + +As the ``django.forms`` ("manipulators") system before it, ``django.newforms`` +is intended to handle HTML form display, validation and redisplay. It's what +you use if you want to perform server-side validation for an HTML form. + +For example, if your Web site has a contact form that visitors can use to +send you e-mail, you'd use this library to implement the display of the HTML +form fields, along with the form validation. Any time you need to use an HTML +``
      ``, you can use this library. + +The library deals with these concepts: + + * **Widget** -- A class that corresponds to an HTML form widget, e.g. + ```` or `` -as_textarea() and as_text() are shortcuts for changing the output widget type: +as_textarea(), as_text() and as_hidden() are shortcuts for changing the output +widget type: >>> f['subject'].as_textarea() u'' >>> f['message'].as_text() u'' +>>> f['message'].as_hidden() +u'' The 'widget' parameter to a Field can also be an instance: >>> class ContactForm(Form): ... subject = CharField() ... message = CharField(widget=Textarea(attrs={'rows': 80, 'cols': 20})) ->>> f = ContactForm() +>>> f = ContactForm(auto_id=False) >>> print f['message'] -Instance-level attrs are *not* carried over to as_textarea() and as_text(): +Instance-level attrs are *not* carried over to as_textarea(), as_text() and +as_hidden(): >>> f['message'].as_text() u'' ->>> f = ContactForm({'subject': 'Hello', 'message': 'I love you.'}) +>>> f = ContactForm({'subject': 'Hello', 'message': 'I love you.'}, auto_id=False) >>> f['subject'].as_textarea() u'' >>> f['message'].as_text() u'' +>>> f['message'].as_hidden() +u'' For a form with a ->>> f = FrameworkForm({'name': 'Django', 'language': 'P'}) +>>> f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False) >>> print f['language'] +Add widget=RadioSelect to use that widget with a ChoiceField. +>>> class FrameworkForm(Form): +... name = CharField() +... language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')], widget=RadioSelect) +>>> f = FrameworkForm(auto_id=False) +>>> print f['language'] +
        +
      • +
      • +
      +>>> print f +Name: +Language:
        +
      • +
      • +
      +>>> print f.as_ul() +
    • Name:
    • +
    • Language:
        +
      • +
      • +
    • + +Regarding auto_id and