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
This commit is contained in:
Brian Rosner 2008-09-01 21:28:32 +00:00
parent ca7db155aa
commit 7c7ad041b3
6 changed files with 149 additions and 22 deletions

View File

@ -231,7 +231,7 @@ class Field(object):
def get_default(self): def get_default(self):
"Returns the default value for this field." "Returns the default value for this field."
if self.default is not NOT_PROVIDED: if self.has_default():
if callable(self.default): if callable(self.default):
return self.default() return self.default()
return force_unicode(self.default, strings_only=True) 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} defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
if self.has_default(): if self.has_default():
defaults['initial'] = self.get_default() defaults['initial'] = self.get_default()
if callable(self.default):
defaults['show_hidden_initial'] = True
if self.choices: if self.choices:
# Fields with choices get special treatment. # Fields with choices get special treatment.
include_blank = self.blank or not (self.has_default() or 'initial' in kwargs) 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 defaults['coerce'] = self.to_python
if self.null: if self.null:
defaults['empty_value'] = None defaults['empty_value'] = None
form_class = forms.TypedChoiceField form_class = forms.TypedChoiceField
# Many of the subclass-specific formfield arguments (min_value, # Many of the subclass-specific formfield arguments (min_value,
# max_value) don't apply for choice fields, so be sure to only pass # max_value) don't apply for choice fields, so be sure to only pass
# the values that TypedChoiceField will understand. # the values that TypedChoiceField will understand.
@ -325,7 +324,6 @@ class Field(object):
'widget', 'label', 'initial', 'help_text', 'widget', 'label', 'initial', 'help_text',
'error_messages'): 'error_messages'):
del kwargs[k] del kwargs[k]
defaults.update(kwargs) defaults.update(kwargs)
return form_class(**defaults) return form_class(**defaults)

View File

@ -28,7 +28,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode, smart_str from django.utils.encoding import smart_unicode, smart_str
from util import ErrorList, ValidationError 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 from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile
__all__ = ( __all__ = (
@ -59,7 +59,7 @@ class Field(object):
creation_counter = 0 creation_counter = 0
def __init__(self, required=True, widget=None, label=None, initial=None, 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. # required -- Boolean that specifies whether the field is required.
# True by default. # True by default.
# widget -- A Widget class, or instance of a Widget class, that should # 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 # initial -- A value to use in this Field's initial display. This value
# is *not* used as a fallback if data isn't given. # is *not* used as a fallback if data isn't given.
# help_text -- An optional string to use as "help text" for this Field. # 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: if label is not None:
label = smart_unicode(label) label = smart_unicode(label)
self.required, self.label, self.initial = required, label, initial self.required, self.label, self.initial = required, label, initial
self.show_hidden_initial = show_hidden_initial
if help_text is None: if help_text is None:
self.help_text = u'' self.help_text = u''
else: else:
@ -840,6 +843,7 @@ class FilePathField(ChoiceField):
self.widget.choices = self.choices self.widget.choices = self.choices
class SplitDateTimeField(MultiValueField): class SplitDateTimeField(MultiValueField):
hidden_widget = SplitHiddenDateTimeWidget
default_error_messages = { default_error_messages = {
'invalid_date': _(u'Enter a valid date.'), 'invalid_date': _(u'Enter a valid date.'),
'invalid_time': _(u'Enter a valid time.'), 'invalid_time': _(u'Enter a valid time.'),

View File

@ -128,6 +128,12 @@ class BaseForm(StrAndUnicode):
""" """
return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name 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): 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()." "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. top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
@ -245,7 +251,7 @@ class BaseForm(StrAndUnicode):
Returns True if data differs from initial. Returns True if data differs from initial.
""" """
return bool(self.changed_data) return bool(self.changed_data)
def _get_changed_data(self): def _get_changed_data(self):
if self._changed_data is None: if self._changed_data is None:
self._changed_data = [] self._changed_data = []
@ -258,7 +264,13 @@ class BaseForm(StrAndUnicode):
for name, field in self.fields.items(): for name, field in self.fields.items():
prefixed_name = self.add_prefix(name) prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
initial_value = self.initial.get(name, field.initial) 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): if field.widget._has_changed(initial_value, data_value):
self._changed_data.append(name) self._changed_data.append(name)
return self._changed_data return self._changed_data
@ -300,6 +312,7 @@ class BoundField(StrAndUnicode):
self.field = field self.field = field
self.name = name self.name = name
self.html_name = form.add_prefix(name) self.html_name = form.add_prefix(name)
self.html_initial_name = form.add_initial_prefix(name)
if self.field.label is None: if self.field.label is None:
self.label = pretty_name(name) self.label = pretty_name(name)
else: else:
@ -308,6 +321,8 @@ class BoundField(StrAndUnicode):
def __unicode__(self): def __unicode__(self):
"""Renders this field as an HTML widget.""" """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() return self.as_widget()
def _errors(self): def _errors(self):
@ -318,7 +333,7 @@ class BoundField(StrAndUnicode):
return self.form.errors.get(self.name, self.form.error_class()) return self.form.errors.get(self.name, self.form.error_class())
errors = property(_errors) 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 Renders the field by rendering the passed widget, adding any HTML
attributes passed as attrs. If no widget is specified, then the attributes passed as attrs. If no widget is specified, then the
@ -330,29 +345,33 @@ class BoundField(StrAndUnicode):
auto_id = self.auto_id auto_id = self.auto_id
if auto_id and 'id' not in attrs and 'id' not in widget.attrs: if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
attrs['id'] = auto_id 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) data = self.form.initial.get(self.name, self.field.initial)
if callable(data): if callable(data):
data = data() data = data()
else: else:
data = self.data data = self.data
return widget.render(self.html_name, data, attrs=attrs) if not only_initial:
name = self.html_name
def as_text(self, attrs=None): else:
name = self.html_initial_name
return widget.render(name, data, attrs=attrs)
def as_text(self, attrs=None, **kwargs):
""" """
Returns a string of HTML for representing this as an <input type="text">. 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>." "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">. 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): def _data(self):
""" """

View File

@ -25,7 +25,8 @@ __all__ = (
'HiddenInput', 'MultipleHiddenInput', 'HiddenInput', 'MultipleHiddenInput',
'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput', 'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget', 'CheckboxSelectMultiple', 'MultiWidget',
'SplitDateTimeWidget',
) )
MEDIA_TYPES = ('css','js') MEDIA_TYPES = ('css','js')
@ -617,7 +618,8 @@ class MultiWidget(Widget):
if initial is None: if initial is None:
initial = [u'' for x in range(0, len(data))] initial = [u'' for x in range(0, len(data))]
else: else:
initial = self.decompress(initial) if not isinstance(initial, list):
initial = self.decompress(initial)
for widget, initial, data in zip(self.widgets, initial, data): for widget, initial, data in zip(self.widgets, initial, data):
if widget._has_changed(initial, data): if widget._has_changed(initial, data):
return True return True
@ -662,3 +664,11 @@ class SplitDateTimeWidget(MultiWidget):
return [value.date(), value.time().replace(microsecond=0)] return [value.date(), value.time().replace(microsecond=0)]
return [None, None] 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)

View File

@ -1,3 +1,7 @@
import datetime
from django import forms
from django.db import models from django.db import models
try: try:
@ -92,6 +96,16 @@ class Price(models.Model):
class MexicanRestaurant(Restaurant): class MexicanRestaurant(Restaurant):
serves_tacos = models.BooleanField() 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': """ __test__ = {'API_TESTS': """
@ -621,4 +635,71 @@ False
>>> formset.errors >>> formset.errors
[{'__all__': [u'Price with this Price and Quantity already exists.']}] [{'__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
"""} """}

View File

@ -1093,7 +1093,7 @@ u'<input type="text" name="date" value="2007-09-17 12:51:34" />'
>>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51)) >>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51))
u'<input type="text" name="date" value="2007-09-17 12:51:00" />' u'<input type="text" name="date" value="2007-09-17 12:51:00" />'
# TimeInput ############################################################### # TimeInput ###################################################################
>>> w = TimeInput() >>> w = TimeInput()
>>> w.render('time', None) >>> w.render('time', None)
@ -1113,5 +1113,20 @@ u'<input type="text" name="time" value="12:51:00" />'
We should be able to initialize from a unicode value. We should be able to initialize from a unicode value.
>>> w.render('time', u'13:12:11') >>> w.render('time', u'13:12:11')
u'<input type="text" name="time" value="13:12:11" />' u'<input type="text" name="time" value="13:12:11" />'
# SplitHiddenDateTimeWidget ###################################################
>>> from django.forms.widgets import SplitHiddenDateTimeWidget
>>> w = SplitHiddenDateTimeWidget()
>>> w.render('date', '')
u'<input type="hidden" name="date_0" /><input type="hidden" name="date_1" />'
>>> w.render('date', d)
u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:34" />'
>>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51, 34))
u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:34" />'
>>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51))
u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:00" />'
""" """