Commit 7c7ad041 authored by Brian Rosner's avatar Brian Rosner
Browse files

Fixed #7975 -- Callable defaults in inline model formsets now work correctly....

Fixed #7975 -- Callable defaults in inline model formsets now work correctly. Based on patch from msaelices. Thanks for your hard work msaelices.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8816 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent ca7db155
Loading
Loading
Loading
Loading
+3 −5
Original line number Diff line number Diff line
@@ -231,7 +231,7 @@ class Field(object):

    def get_default(self):
        "Returns the default value for this field."
        if self.default is not NOT_PROVIDED:
        if self.has_default():
            if callable(self.default):
                return self.default()
            return force_unicode(self.default, strings_only=True)
@@ -306,7 +306,8 @@ class Field(object):
        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
        if self.has_default():
            defaults['initial'] = self.get_default()

            if callable(self.default):
                defaults['show_hidden_initial'] = True
        if self.choices:
            # Fields with choices get special treatment. 
            include_blank = self.blank or not (self.has_default() or 'initial' in kwargs)
@@ -314,9 +315,7 @@ class Field(object):
            defaults['coerce'] = self.to_python
            if self.null:
                defaults['empty_value'] = None
            
            form_class = forms.TypedChoiceField
            
            # Many of the subclass-specific formfield arguments (min_value,
            # max_value) don't apply for choice fields, so be sure to only pass
            # the values that TypedChoiceField will understand.
@@ -325,7 +324,6 @@ class Field(object):
                             'widget', 'label', 'initial', 'help_text',
                             'error_messages'):
                    del kwargs[k]
        
        defaults.update(kwargs)
        return form_class(**defaults)

+6 −2
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode, smart_str

from util import ErrorList, ValidationError
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput, SplitHiddenDateTimeWidget
from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile

__all__ = (
@@ -59,7 +59,7 @@ class Field(object):
    creation_counter = 0

    def __init__(self, required=True, widget=None, label=None, initial=None,
                 help_text=None, error_messages=None):
                 help_text=None, error_messages=None, show_hidden_initial=False):
        # required -- Boolean that specifies whether the field is required.
        #             True by default.
        # widget -- A Widget class, or instance of a Widget class, that should
@@ -73,9 +73,12 @@ class Field(object):
        # initial -- A value to use in this Field's initial display. This value
        #            is *not* used as a fallback if data isn't given.
        # help_text -- An optional string to use as "help text" for this Field.
        # show_hidden_initial -- Boolean that specifies if it is needed to render a
        #                        hidden widget with initial value after widget.
        if label is not None:
            label = smart_unicode(label)
        self.required, self.label, self.initial = required, label, initial
        self.show_hidden_initial = show_hidden_initial
        if help_text is None:
            self.help_text = u''
        else:
@@ -840,6 +843,7 @@ class FilePathField(ChoiceField):
        self.widget.choices = self.choices

class SplitDateTimeField(MultiValueField):
    hidden_widget = SplitHiddenDateTimeWidget
    default_error_messages = {
        'invalid_date': _(u'Enter a valid date.'),
        'invalid_time': _(u'Enter a valid time.'),
+31 −12
Original line number Diff line number Diff line
@@ -128,6 +128,12 @@ class BaseForm(StrAndUnicode):
        """
        return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name

    def add_initial_prefix(self, field_name):
        """
        Add a 'initial' prefix for checking dynamic initial values
        """
        return u'initial-%s' % self.add_prefix(field_name)

    def _html_output(self, normal_row, error_row, row_ender, help_text_html, 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.
@@ -258,7 +264,13 @@ class BaseForm(StrAndUnicode):
            for name, field in self.fields.items():
                prefixed_name = self.add_prefix(name)
                data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
                if not field.show_hidden_initial:
                    initial_value = self.initial.get(name, field.initial)
                else:
                    initial_prefixed_name = self.add_initial_prefix(name)
                    hidden_widget = field.hidden_widget()
                    initial_value = hidden_widget.value_from_datadict(
                        self.data, self.files, initial_prefixed_name)
                if field.widget._has_changed(initial_value, data_value):
                    self._changed_data.append(name)
        return self._changed_data
@@ -300,6 +312,7 @@ class BoundField(StrAndUnicode):
        self.field = field
        self.name = name
        self.html_name = form.add_prefix(name)
        self.html_initial_name = form.add_initial_prefix(name)
        if self.field.label is None:
            self.label = pretty_name(name)
        else:
@@ -308,6 +321,8 @@ class BoundField(StrAndUnicode):

    def __unicode__(self):
        """Renders this field as an HTML widget."""
        if self.field.show_hidden_initial:
            return self.as_widget() + self.as_hidden(only_initial=True)
        return self.as_widget()

    def _errors(self):
@@ -318,7 +333,7 @@ class BoundField(StrAndUnicode):
        return self.form.errors.get(self.name, self.form.error_class())
    errors = property(_errors)

    def as_widget(self, widget=None, attrs=None):
    def as_widget(self, widget=None, attrs=None, only_initial=False):
        """
        Renders the field by rendering the passed widget, adding any HTML
        attributes passed as attrs.  If no widget is specified, then the
@@ -330,29 +345,33 @@ class BoundField(StrAndUnicode):
        auto_id = self.auto_id
        if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
            attrs['id'] = auto_id
        if not self.form.is_bound:
        if not self.form.is_bound or only_initial:
            data = self.form.initial.get(self.name, self.field.initial)
            if callable(data):
                data = data()
        else:
            data = self.data
        return widget.render(self.html_name, data, attrs=attrs)
        if not only_initial:
            name = self.html_name
        else:
            name = self.html_initial_name
        return widget.render(name, data, attrs=attrs)
        
    def as_text(self, attrs=None):
    def as_text(self, attrs=None, **kwargs):
        """
        Returns a string of HTML for representing this as an <input type="text">.
        """
        return self.as_widget(TextInput(), attrs)
        return self.as_widget(TextInput(), attrs, **kwargs)

    def as_textarea(self, attrs=None):
    def as_textarea(self, attrs=None, **kwargs):
        "Returns a string of HTML for representing this as a <textarea>."
        return self.as_widget(Textarea(), attrs)
        return self.as_widget(Textarea(), attrs, **kwargs)

    def as_hidden(self, attrs=None):
    def as_hidden(self, attrs=None, **kwargs):
        """
        Returns a string of HTML for representing this as an <input type="hidden">.
        """
        return self.as_widget(self.field.hidden_widget(), attrs)
        return self.as_widget(self.field.hidden_widget(), attrs, **kwargs)

    def _data(self):
        """
+12 −2
Original line number Diff line number Diff line
@@ -25,7 +25,8 @@ __all__ = (
    'HiddenInput', 'MultipleHiddenInput',
    'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
    'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
    'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
    'CheckboxSelectMultiple', 'MultiWidget',
    'SplitDateTimeWidget',
)

MEDIA_TYPES = ('css','js')
@@ -617,6 +618,7 @@ class MultiWidget(Widget):
        if initial is None:
            initial = [u'' for x in range(0, len(data))]
        else:
            if not isinstance(initial, list):
                initial = self.decompress(initial)
        for widget, initial, data in zip(self.widgets, initial, data):
            if widget._has_changed(initial, data):
@@ -662,3 +664,11 @@ class SplitDateTimeWidget(MultiWidget):
            return [value.date(), value.time().replace(microsecond=0)]
        return [None, None]

class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
    """
    A Widget that splits datetime input into two <input type="hidden"> inputs.
    """
    def __init__(self, attrs=None):
        widgets = (HiddenInput(attrs=attrs), HiddenInput(attrs=attrs))
        super(SplitDateTimeWidget, self).__init__(widgets, attrs)
        
 No newline at end of file
+81 −0
Original line number Diff line number Diff line

import datetime

from django import forms
from django.db import models

try:
@@ -92,6 +96,16 @@ class Price(models.Model):
class MexicanRestaurant(Restaurant):
    serves_tacos = models.BooleanField()

# models for testing callable defaults (see bug #7975). If you define a model
# with a callable default value, you cannot rely on the initial value in a
# form.
class Person(models.Model):
    name = models.CharField(max_length=128)

class Membership(models.Model):
    person = models.ForeignKey(Person)
    date_joined = models.DateTimeField(default=datetime.datetime.now)
    karma = models.IntegerField()

__test__ = {'API_TESTS': """

@@ -621,4 +635,71 @@ False
>>> formset.errors
[{'__all__': [u'Price with this Price and Quantity already exists.']}]

# Use of callable defaults (see bug #7975).

>>> person = Person.objects.create(name='Ringo')
>>> FormSet = inlineformset_factory(Person, Membership, can_delete=False, extra=1)
>>> formset = FormSet(instance=person)

# Django will render a hidden field for model fields that have a callable
# default. This is required to ensure the value is tested for change correctly
# when determine what extra forms have changed to save.

>>> form = formset.forms[0] # this formset only has one form
>>> now = form.fields['date_joined'].initial
>>> print form.as_p()
<p><label for="id_membership_set-0-date_joined">Date joined:</label> <input type="text" name="membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /><input type="hidden" name="initial-membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /></p>
<p><label for="id_membership_set-0-karma">Karma:</label> <input type="text" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p>

# test for validation with callable defaults. Validations rely on hidden fields

>>> data = {
...     'membership_set-TOTAL_FORMS': '1',
...     'membership_set-INITIAL_FORMS': '0',
...     'membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
...     'membership_set-0-karma': '',
... }
>>> formset = FormSet(data, instance=person)
>>> formset.is_valid()
True

# now test for when the data changes

>>> one_day_later = now + datetime.timedelta(days=1)
>>> filled_data = {
...     'membership_set-TOTAL_FORMS': '1',
...     'membership_set-INITIAL_FORMS': '0',
...     'membership_set-0-date_joined': unicode(one_day_later.strftime('%Y-%m-%d %H:%M:%S')),
...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
...     'membership_set-0-karma': '',
... }
>>> formset = FormSet(filled_data, instance=person)
>>> formset.is_valid()
False

# now test with split datetime fields

>>> class MembershipForm(forms.ModelForm):
...     date_joined = forms.SplitDateTimeField(initial=now)
...     class Meta:
...         model = Membership
...     def __init__(self, **kwargs):
...         super(MembershipForm, self).__init__(**kwargs)
...         self.fields['date_joined'].widget = forms.SplitDateTimeWidget()

>>> FormSet = inlineformset_factory(Person, Membership, form=MembershipForm, can_delete=False, extra=1)
>>> data = {
...     'membership_set-TOTAL_FORMS': '1',
...     'membership_set-INITIAL_FORMS': '0',
...     'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')),
...     'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')),
...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
...     'membership_set-0-karma': '',
... }
>>> formset = FormSet(data, instance=person)
>>> formset.is_valid()
True


"""}
Loading