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):
"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)

View File

@ -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.'),

View File

@ -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.
@ -245,7 +251,7 @@ class BaseForm(StrAndUnicode):
Returns True if data differs from initial.
"""
return bool(self.changed_data)
def _get_changed_data(self):
if self._changed_data is None:
self._changed_data = []
@ -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)
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):
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)
def as_text(self, attrs=None):
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, **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):
"""

View File

@ -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,7 +618,8 @@ class MultiWidget(Widget):
if initial is None:
initial = [u'' for x in range(0, len(data))]
else:
initial = self.decompress(initial)
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):
return True
@ -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)

View File

@ -1,3 +1,7 @@
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
"""}

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))
u'<input type="text" name="date" value="2007-09-17 12:51:00" />'
# TimeInput ###############################################################
# TimeInput ###################################################################
>>> w = TimeInput()
>>> 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.
>>> w.render('time', u'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" />'
"""