mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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.'), | ||||
|   | ||||
| @@ -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): | ||||
|         """ | ||||
|   | ||||
| @@ -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) | ||||
|          | ||||
| @@ -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 | ||||
|  | ||||
|  | ||||
| """} | ||||
|   | ||||
| @@ -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" />' | ||||
|  | ||||
| """ | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user