diff options
| author | Claude Paroz <claude@2xlibre.net> | 2013-01-25 20:50:46 +0100 |
|---|---|---|
| committer | Claude Paroz <claude@2xlibre.net> | 2013-01-25 20:50:46 +0100 |
| commit | ebb504db692cac496f4f45762d1d14644c9fa6fa (patch) | |
| tree | 1a1ac132d765f6e93c390fe532f9b39d1edee096 | |
| parent | ce27fb198dcce5dad47de83fc81119d3bb6567ce (diff) | |
Moved has_changed logic from widget to form field
Refs #16612. Thanks Aymeric Augustin for the suggestion.
| -rw-r--r-- | django/contrib/admin/widgets.py | 14 | ||||
| -rw-r--r-- | django/contrib/gis/admin/widgets.py | 24 | ||||
| -rw-r--r-- | django/contrib/gis/forms/fields.py | 26 | ||||
| -rw-r--r-- | django/contrib/gis/tests/geoadmin/tests.py | 2 | ||||
| -rw-r--r-- | django/forms/extras/widgets.py | 8 | ||||
| -rw-r--r-- | django/forms/fields.py | 77 | ||||
| -rw-r--r-- | django/forms/forms.py | 8 | ||||
| -rw-r--r-- | django/forms/models.py | 9 | ||||
| -rw-r--r-- | django/forms/widgets.py | 92 | ||||
| -rw-r--r-- | docs/releases/1.6.txt | 6 | ||||
| -rw-r--r-- | tests/regressiontests/admin_widgets/tests.py | 7 | ||||
| -rw-r--r-- | tests/regressiontests/forms/tests/extra.py | 21 | ||||
| -rw-r--r-- | tests/regressiontests/forms/tests/fields.py | 90 | ||||
| -rw-r--r-- | tests/regressiontests/forms/tests/widgets.py | 97 |
14 files changed, 230 insertions, 251 deletions
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 1e6277fb87..a3887740d8 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -213,17 +213,6 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): if value: return value.split(',') - def _has_changed(self, initial, data): - if initial is None: - initial = [] - if data is None: - data = [] - if len(initial) != len(data): - return True - for pk1, pk2 in zip(initial, data): - if force_text(pk1) != force_text(pk2): - return True - return False class RelatedFieldWidgetWrapper(forms.Widget): """ @@ -279,9 +268,6 @@ class RelatedFieldWidgetWrapper(forms.Widget): def value_from_datadict(self, data, files, name): return self.widget.value_from_datadict(data, files, name) - def _has_changed(self, initial, data): - return self.widget._has_changed(initial, data) - def id_for_label(self, id_): return self.widget.id_for_label(id_) diff --git a/django/contrib/gis/admin/widgets.py b/django/contrib/gis/admin/widgets.py index f4379be7f3..a06933660f 100644 --- a/django/contrib/gis/admin/widgets.py +++ b/django/contrib/gis/admin/widgets.py @@ -7,7 +7,7 @@ from django.utils import six from django.utils import translation from django.contrib.gis.gdal import OGRException -from django.contrib.gis.geos import GEOSGeometry, GEOSException, fromstr +from django.contrib.gis.geos import GEOSGeometry, GEOSException # Creating a template context that contains Django settings # values needed by admin map templates. @@ -117,25 +117,3 @@ class OpenLayersWidget(Textarea): raise TypeError map_options[js_name] = value return map_options - - def _has_changed(self, initial, data): - """ Compare geographic value of data with its initial value. """ - - # Ensure we are dealing with a geographic object - if isinstance(initial, six.string_types): - try: - initial = GEOSGeometry(initial) - except (GEOSException, ValueError): - initial = None - - # Only do a geographic comparison if both values are available - if initial and data: - data = fromstr(data) - data.transform(initial.srid) - # If the initial value was not added by the browser, the geometry - # provided may be slightly different, the first time it is saved. - # The comparison is done with a very low tolerance. - return not initial.equals_exact(data, tolerance=0.000001) - else: - # Check for change of state of existence - return bool(initial) != bool(data) diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py index cefb6830ba..ab2e37f1e1 100644 --- a/django/contrib/gis/forms/fields.py +++ b/django/contrib/gis/forms/fields.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals from django import forms +from django.utils import six from django.utils.translation import ugettext_lazy as _ # While this couples the geographic forms to the GEOS library, # it decouples from database (by not importing SpatialBackend). -from django.contrib.gis.geos import GEOSException, GEOSGeometry +from django.contrib.gis.geos import GEOSException, GEOSGeometry, fromstr + class GeometryField(forms.Field): """ @@ -73,3 +75,25 @@ class GeometryField(forms.Field): raise forms.ValidationError(self.error_messages['transform_error']) return geom + + def _has_changed(self, initial, data): + """ Compare geographic value of data with its initial value. """ + + # Ensure we are dealing with a geographic object + if isinstance(initial, six.string_types): + try: + initial = GEOSGeometry(initial) + except (GEOSException, ValueError): + initial = None + + # Only do a geographic comparison if both values are available + if initial and data: + data = fromstr(data) + data.transform(initial.srid) + # If the initial value was not added by the browser, the geometry + # provided may be slightly different, the first time it is saved. + # The comparison is done with a very low tolerance. + return not initial.equals_exact(data, tolerance=0.000001) + else: + # Check for change of state of existence + return bool(initial) != bool(data) diff --git a/django/contrib/gis/tests/geoadmin/tests.py b/django/contrib/gis/tests/geoadmin/tests.py index 6fadebdb9a..669914bdea 100644 --- a/django/contrib/gis/tests/geoadmin/tests.py +++ b/django/contrib/gis/tests/geoadmin/tests.py @@ -38,7 +38,7 @@ class GeoAdminTest(TestCase): """ Check that changes are accurately noticed by OpenLayersWidget. """ geoadmin = admin.site._registry[City] form = geoadmin.get_changelist_form(None)() - has_changed = form.fields['point'].widget._has_changed + has_changed = form.fields['point']._has_changed initial = Point(13.4197458572965953, 52.5194108501149799, srid=4326) data_same = "SRID=3857;POINT(1493879.2754093995 6894592.019687599)" diff --git a/django/forms/extras/widgets.py b/django/forms/extras/widgets.py index c5ca1424c8..e939a8f665 100644 --- a/django/forms/extras/widgets.py +++ b/django/forms/extras/widgets.py @@ -135,11 +135,3 @@ class SelectDateWidget(Widget): s = Select(choices=choices) select_html = s.render(field % name, val, local_attrs) return select_html - - def _has_changed(self, initial, data): - try: - input_format = get_format('DATE_INPUT_FORMATS')[0] - data = datetime_safe.datetime.strptime(data, input_format).date() - except (TypeError, ValueError): - pass - return super(SelectDateWidget, self)._has_changed(initial, data) diff --git a/django/forms/fields.py b/django/forms/fields.py index 4438812a37..1e9cbcb4d9 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -175,6 +175,25 @@ class Field(object): """ return {} + def _has_changed(self, initial, data): + """ + Return True if data differs from initial. + """ + # For purposes of seeing whether something has changed, None is + # the same as an empty string, if the data or inital value we get + # is None, replace it w/ ''. + if data is None: + data_value = '' + else: + data_value = data + if initial is None: + initial_value = '' + else: + initial_value = initial + if force_text(initial_value) != force_text(data_value): + return True + return False + def __deepcopy__(self, memo): result = copy.copy(self) memo[id(self)] = result @@ -348,6 +367,13 @@ class BaseTemporalField(Field): def strptime(self, value, format): raise NotImplementedError('Subclasses must define this method.') + def _has_changed(self, initial, data): + try: + data = self.to_python(data) + except ValidationError: + return True + return self.to_python(initial) != data + class DateField(BaseTemporalField): widget = DateInput input_formats = formats.get_format_lazy('DATE_INPUT_FORMATS') @@ -371,6 +397,7 @@ class DateField(BaseTemporalField): def strptime(self, value, format): return datetime.datetime.strptime(value, format).date() + class TimeField(BaseTemporalField): widget = TimeInput input_formats = formats.get_format_lazy('TIME_INPUT_FORMATS') @@ -529,6 +556,12 @@ class FileField(Field): return initial return data + def _has_changed(self, initial, data): + if data is None: + return False + return True + + class ImageField(FileField): default_error_messages = { 'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."), @@ -618,6 +651,7 @@ class URLField(CharField): value = urlunsplit(url_fields) return value + class BooleanField(Field): widget = CheckboxInput @@ -636,6 +670,15 @@ class BooleanField(Field): raise ValidationError(self.error_messages['required']) return value + def _has_changed(self, initial, data): + # Sometimes data or initial could be None or '' which should be the + # same thing as False. + if initial == 'False': + # show_hidden_initial may have transformed False to 'False' + initial = False + return bool(initial) != bool(data) + + class NullBooleanField(BooleanField): """ A field whose valid values are None, True and False. Invalid values are @@ -660,6 +703,15 @@ class NullBooleanField(BooleanField): def validate(self, value): pass + def _has_changed(self, initial, data): + # None (unknown) and False (No) are not the same + if initial is not None: + initial = bool(initial) + if data is not None: + data = bool(data) + return initial != data + + class ChoiceField(Field): widget = Select default_error_messages = { @@ -739,6 +791,7 @@ class TypedChoiceField(ChoiceField): def validate(self, value): pass + class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput widget = SelectMultiple @@ -765,6 +818,18 @@ class MultipleChoiceField(ChoiceField): if not self.valid_value(val): raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) + def _has_changed(self, initial, data): + if initial is None: + initial = [] + if data is None: + data = [] + if len(initial) != len(data): + return True + initial_set = set([force_text(value) for value in initial]) + data_set = set([force_text(value) for value in data]) + return data_set != initial_set + + class TypedMultipleChoiceField(MultipleChoiceField): def __init__(self, *args, **kwargs): self.coerce = kwargs.pop('coerce', lambda val: val) @@ -899,6 +964,18 @@ class MultiValueField(Field): """ raise NotImplementedError('Subclasses must implement this method.') + def _has_changed(self, initial, data): + if initial is None: + initial = ['' for x in range(0, len(data))] + else: + if not isinstance(initial, list): + initial = self.widget.decompress(initial) + for field, initial, data in zip(self.fields, initial, data): + if field._has_changed(initial, data): + return True + return False + + class FilePathField(ChoiceField): def __init__(self, path, match=None, recursive=False, allow_files=True, allow_folders=False, required=True, widget=None, label=None, diff --git a/django/forms/forms.py b/django/forms/forms.py index 3299c2becc..f532391296 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -341,7 +341,13 @@ class BaseForm(object): 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 hasattr(field.widget, '_has_changed'): + warnings.warn("The _has_changed method on widgets is deprecated," + " define it at field level instead.", + PendingDeprecationWarning, stacklevel=2) + if field.widget._has_changed(initial_value, data_value): + self._changed_data.append(name) + elif field._has_changed(initial_value, data_value): self._changed_data.append(name) return self._changed_data changed_data = property(_get_changed_data) diff --git a/django/forms/models.py b/django/forms/models.py index 03a14dc9ff..837da74814 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -858,15 +858,12 @@ def inlineformset_factory(parent_model, model, form=ModelForm, # Fields ##################################################################### -class InlineForeignKeyHiddenInput(HiddenInput): - def _has_changed(self, initial, data): - return False - class InlineForeignKeyField(Field): """ A basic integer field that deals with validating the given value to a given parent instance in an inline. """ + widget = HiddenInput default_error_messages = { 'invalid_choice': _('The inline foreign key did not match the parent instance primary key.'), } @@ -881,7 +878,6 @@ class InlineForeignKeyField(Field): else: kwargs["initial"] = self.parent_instance.pk kwargs["required"] = False - kwargs["widget"] = InlineForeignKeyHiddenInput super(InlineForeignKeyField, self).__init__(*args, **kwargs) def clean(self, value): @@ -899,6 +895,9 @@ class InlineForeignKeyField(Field): raise ValidationError(self.error_messages['invalid_choice']) return self.parent_instance + def _has_changed(self, initial, data): + return False + class ModelChoiceIterator(object): def __init__(self, field): self.field = field diff --git a/django/forms/widgets.py b/django/forms/widgets.py index d6ea56f0c8..303844d44b 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -208,25 +208,6 @@ class Widget(six.with_metaclass(MediaDefiningClass)): """ return data.get(name, None) - def _has_changed(self, initial, data): - """ - Return True if data differs from initial. - """ - # For purposes of seeing whether something has changed, None is - # the same as an empty string, if the data or inital value we get - # is None, replace it w/ ''. - if data is None: - data_value = '' - else: - data_value = data - if initial is None: - initial_value = '' - else: - initial_value = initial - if force_text(initial_value) != force_text(data_value): - return True - return False - def id_for_label(self, id_): """ Returns the HTML ID attribute of this Widget for use by a <label>, @@ -325,10 +306,6 @@ class FileInput(Input): "File widgets take data from FILES, not POST" return files.get(name, None) - def _has_changed(self, initial, data): - if data is None: - return False - return True FILE_INPUT_CONTRADICTION = object() @@ -426,17 +403,6 @@ class DateInput(TextInput): return value.strftime(self.format) return value - def _has_changed(self, initial, data): - # If our field has show_hidden_initial=True, initial will be a string - # formatted by HiddenInput using formats.localize_input, which is not - # necessarily the format used for this widget. Attempt to convert it. - try: - input_format = formats.get_format('DATE_INPUT_FORMATS')[0] - initial = datetime.datetime.strptime(initial, input_format).date() - except (TypeError, ValueError): - pass - return super(DateInput, self)._has_changed(self._format_value(initial), data) - class DateTimeInput(TextInput): def __init__(self, attrs=None, format=None): @@ -456,17 +422,6 @@ class DateTimeInput(TextInput): return value.strftime(self.format) return value - def _has_changed(self, initial, data): - # If our field has show_hidden_initial=True, initial will be a string - # formatted by HiddenInput using formats.localize_input, which is not - # necessarily the format used for this widget. Attempt to convert it. - try: - input_format = formats.get_format('DATETIME_INPUT_FORMATS')[0] - initial = datetime.datetime.strptime(initial, input_format) - except (TypeError, ValueError): - pass - return super(DateTimeInput, self)._has_changed(self._format_value(initial), data) - class TimeInput(TextInput): def __init__(self, attrs=None, format=None): @@ -485,17 +440,6 @@ class TimeInput(TextInput): return value.strftime(self.format) return value - def _has_changed(self, initial, data): - # If our field has show_hidden_initial=True, initial will be a string - # formatted by HiddenInput using formats.localize_input, which is not - # necessarily the format used for this widget. Attempt to convert it. - try: - input_format = formats.get_format('TIME_INPUT_FORMATS')[0] - initial = datetime.datetime.strptime(initial, input_format).time() - except (TypeError, ValueError): - pass - return super(TimeInput, self)._has_changed(self._format_value(initial), data) - # Defined at module level so that CheckboxInput is picklable (#17976) def boolean_check(v): @@ -530,13 +474,6 @@ class CheckboxInput(Widget): value = values.get(value.lower(), value) return bool(value) - def _has_changed(self, initial, data): - # Sometimes data or initial could be None or '' which should be the - # same thing as False. - if initial == 'False': - # show_hidden_initial may have transformed False to 'False' - initial = False - return bool(initial) != bool(data) class Select(Widget): allow_multiple_selected = False @@ -612,14 +549,6 @@ class NullBooleanSelect(Select): 'False': False, False: False}.get(value, None) - def _has_changed(self, initial, data): - # For a NullBooleanSelect, None (unknown) and False (No) - # are not the same - if initial is not None: - initial = bool(initial) - if data is not None: - data = bool(data) - return initial != data class SelectMultiple(Select): allow_multiple_selected = True @@ -639,16 +568,6 @@ class SelectMultiple(Select): return data.getlist(name) return data.get(name, None) - def _has_changed(self, initial, data): - if initial is None: - initial = [] - if data is None: - data = [] - if len(initial) != len(data): - return True - initial_set = set([force_text(value) for value in initial]) - data_set = set([force_text(value) for value in data]) - return data_set != initial_set @python_2_unicode_compatible class RadioInput(SubWidget): @@ -844,17 +763,6 @@ class MultiWidget(Widget): def value_from_datadict(self, data, files, name): return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)] - def _has_changed(self, initial, data): - if initial is None: - initial = ['' 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): - return True - return False - def format_output(self, rendered_widgets): """ Given a list of rendered widgets (as strings), returns a Unicode string diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 89d7bb3c05..29ecad3e9f 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -67,3 +67,9 @@ If you're relying on this feature, you should add ``'django.middleware.common.BrokenLinkEmailsMiddleware'`` to your :setting:`MIDDLEWARE_CLASSES` setting and remove ``SEND_BROKEN_LINK_EMAILS`` from your settings. + +``_has_changed`` method on widgets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you defined your own form widgets and defined the ``_has_changed`` method +on a widget, you should now define this method on the form field itself. diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py index 76aa9f448f..452387078d 100644 --- a/tests/regressiontests/admin_widgets/tests.py +++ b/tests/regressiontests/admin_widgets/tests.py @@ -425,13 +425,6 @@ class ManyToManyRawIdWidgetTest(DjangoTestCase): '<input type="text" name="test" value="%(m1pk)s" class="vManyToManyRawIdAdminField" /><a href="/widget_admin/admin_widgets/member/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % dict(admin_static_prefix(), m1pk=m1.pk) ) - self.assertEqual(w._has_changed(None, None), False) - self.assertEqual(w._has_changed([], None), False) - self.assertEqual(w._has_changed(None, ['1']), True) - self.assertEqual(w._has_changed([1, 2], ['1', '2']), False) - self.assertEqual(w._has_changed([1, 2], ['1']), True) - self.assertEqual(w._has_changed([1, 2], ['1', '3']), True) - def test_m2m_related_model_not_in_admin(self): # M2M relationship with model not registered with admin site. Raw ID # widget should have no magnifying glass link. See #16542 diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py index 07acb29741..762b774a93 100644 --- a/tests/regressiontests/forms/tests/extra.py +++ b/tests/regressiontests/forms/tests/extra.py @@ -428,6 +428,23 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): # If insufficient data is provided, None is substituted self.assertFormErrors(['This field is required.'], f.clean, ['some text',['JP']]) + # test with no initial data + self.assertTrue(f._has_changed(None, ['some text', ['J','P'], ['2007-04-25','6:24:00']])) + + # test when the data is the same as initial + self.assertFalse(f._has_changed('some text,JP,2007-04-25 06:24:00', + ['some text', ['J','P'], ['2007-04-25','6:24:00']])) + + # test when the first widget's data has changed + self.assertTrue(f._has_changed('some text,JP,2007-04-25 06:24:00', + ['other text', ['J','P'], ['2007-04-25','6:24:00']])) + + # test when the last widget's data has changed. this ensures that it is not + # short circuiting while testing the widgets. + self.assertTrue(f._has_changed('some text,JP,2007-04-25 06:24:00', + ['some text', ['J','P'], ['2009-04-25','11:44:00']])) + + class ComplexFieldForm(Form): field1 = ComplexField(widget=w) @@ -725,8 +742,8 @@ class FormsExtraL10NTestCase(TestCase): def test_l10n_date_changed(self): """ - Ensure that SelectDateWidget._has_changed() works correctly with a - localized date format. + Ensure that DateField._has_changed() with SelectDateWidget works + correctly with a localized date format. Refs #17165. """ # With Field.show_hidden_initial=False ----------------------- diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index e17d976fcf..7deb345a33 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -35,6 +35,7 @@ from decimal import Decimal from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * from django.test import SimpleTestCase +from django.utils import formats from django.utils import six from django.utils._os import upath @@ -362,6 +363,13 @@ class FieldsTests(SimpleTestCase): f = DateField() self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, 'a\x00b') + def test_datefield_changed(self): + format = '%d/%m/%Y' + f = DateField(input_formats=[format]) + d = datetime.date(2007, 9, 17) + self.assertFalse(f._has_changed(d, '17/09/2007')) + self.assertFalse(f._has_changed(d.strftime(format), '17/09/2007')) + # TimeField ################################################################### def test_timefield_1(self): @@ -388,6 +396,18 @@ class FieldsTests(SimpleTestCase): self.assertEqual(datetime.time(14, 25, 59), f.clean(' 14:25:59 ')) self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ' ') + def test_timefield_changed(self): + t1 = datetime.time(12, 51, 34, 482548) + t2 = datetime.time(12, 51) + format = '%H:%M' + f = TimeField(input_formats=[format]) + self.assertTrue(f._has_changed(t1, '12:51')) + self.assertFalse(f._has_changed(t2, '12:51')) + + format = '%I:%M %p' + f = TimeField(input_formats=[format]) + self.assertFalse(f._has_changed(t2.strftime(format), '12:51 PM')) + # DateTimeField ############################################################### def test_datetimefield_1(self): @@ -446,6 +466,15 @@ class FieldsTests(SimpleTestCase): def test_datetimefield_5(self): f = DateTimeField(input_formats=['%Y.%m.%d %H:%M:%S.%f']) self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('2006.10.25 14:30:45.0002')) + + def test_datetimefield_changed(self): + format = '%Y %m %d %I:%M %p' + f = DateTimeField(input_formats=[format]) + d = datetime.datetime(2006, 9, 17, 14, 30, 0) + self.assertFalse(f._has_changed(d, '2006 09 17 2:30 PM')) + # Initial value may be a string from a hidden input + self.assertFalse(f._has_changed(d.strftime(format), '2006 09 17 2:30 PM')) + # RegexField ################################################################## def test_regexfield_1(self): @@ -566,6 +595,29 @@ class FieldsTests(SimpleTestCase): self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', b'')))) + def test_filefield_changed(self): + ''' + Test for the behavior of _has_changed for FileField. The value of data will + more than likely come from request.FILES. The value of initial data will + likely be a filename stored in the database. Since its value is of no use to + a FileField it is ignored. + ''' + f = FileField() + + # No file was uploaded and no initial data. + self.assertFalse(f._has_changed('', None)) + + # A file was uploaded and no initial data. + self.assertTrue(f._has_changed('', {'filename': 'resume.txt', 'content': 'My resume'})) + + # A file was not uploaded, but there is initial data + self.assertFalse(f._has_changed('resume.txt', None)) + + # A file was uploaded and there is initial data (file identity is not dealt + # with here) + self.assertTrue(f._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'})) + + # URLField ################################################################## def test_urlfield_1(self): @@ -709,6 +761,18 @@ class FieldsTests(SimpleTestCase): def test_boolean_picklable(self): self.assertIsInstance(pickle.loads(pickle.dumps(BooleanField())), BooleanField) + def test_booleanfield_changed(self): + f = BooleanField() + self.assertFalse(f._has_changed(None, None)) + self.assertFalse(f._has_changed(None, '')) + self.assertFalse(f._has_changed('', None)) + self.assertFalse(f._has_changed('', '')) + self.assertTrue(f._has_changed(False, 'on')) + self.assertFalse(f._has_changed(True, 'on')) + self.assertTrue(f._has_changed(True, '')) + # Initial value may have mutated to a string due to show_hidden_initial (#19537) + self.assertTrue(f._has_changed('False', 'on')) + # ChoiceField ################################################################# def test_choicefield_1(self): @@ -825,6 +889,16 @@ class FieldsTests(SimpleTestCase): self.assertEqual(False, f.cleaned_data['nullbool1']) self.assertEqual(None, f.cleaned_data['nullbool2']) + def test_nullbooleanfield_changed(self): + f = NullBooleanField() + self.assertTrue(f._has_changed(False, None)) + self.assertTrue(f._has_changed(None, False)) + self.assertFalse(f._has_changed(None, None)) + self.assertFalse(f._has_changed(False, False)) + self.assertTrue(f._has_changed(True, False)) + self.assertTrue(f._has_changed(True, None)) + self.assertTrue(f._has_changed(True, False)) + # MultipleChoiceField ######################################################### def test_multiplechoicefield_1(self): @@ -866,6 +940,16 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['6']) self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['1','6']) + def test_multiplechoicefield_changed(self): + f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two'), ('3', 'Three')]) + self.assertFalse(f._has_changed(None, None)) + self.assertFalse(f._has_changed([], None)) + self.assertTrue(f._has_changed(None, ['1'])) + self.assertFalse(f._has_changed([1, 2], ['1', '2'])) + self.assertFalse(f._has_changed([2, 1], ['1', '2'])) + self.assertTrue(f._has_changed([1, 2], ['1'])) + self.assertTrue(f._has_changed([1, 2], ['1', '3'])) + # TypedMultipleChoiceField ############################################################ # TypedMultipleChoiceField is just like MultipleChoiceField, except that coerced types # will be returned: @@ -1048,3 +1132,9 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10', '']) self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10']) self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, ['', '07:30']) + + def test_splitdatetimefield_changed(self): + f = SplitDateTimeField(input_date_formats=['%d/%m/%Y']) + self.assertTrue(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['2008-05-06', '12:40:00'])) + self.assertFalse(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:40'])) + self.assertTrue(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:41'])) diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 7a2961358a..6aa56ec8b0 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -148,25 +148,6 @@ class FormsWidgetTestCase(TestCase): self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '<input type="file" class="fun" name="email" />') - # Test for the behavior of _has_changed for FileInput. The value of data will - # more than likely come from request.FILES. The value of initial data will - # likely be a filename stored in the database. Since its value is of no use to - # a FileInput it is ignored. - w = FileInput() - - # No file was uploaded and no initial data. - self.assertFalse(w._has_changed('', None)) - - # A file was uploaded and no initial data. - self.assertTrue(w._has_changed('', {'filename': 'resume.txt', 'content': 'My resume'})) - - # A file was not uploaded, but there is initial data - self.assertFalse(w._has_changed('resume.txt', None)) - - # A file was uploaded and there is initial data (file identity is not dealt - # with here) - self.assertTrue(w._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'})) - def test_textarea(self): w = Textarea() self.assertHTMLEqual(w.render('msg', ''), '<textarea rows="10" cols="40" name="msg"></textarea>') @@ -233,16 +214,6 @@ class FormsWidgetTestCase(TestCase): self.assertIsInstance(value, bool) self.assertTrue(value) - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed(None, '')) - self.assertFalse(w._has_changed('', None)) - self.assertFalse(w._has_changed('', '')) - self.assertTrue(w._has_changed(False, 'on')) - self.assertFalse(w._has_changed(True, 'on')) - self.assertTrue(w._has_changed(True, '')) - # Initial value may have mutated to a string due to show_hidden_initial (#19537) - self.assertTrue(w._has_changed('False', 'on')) - def test_select(self): w = Select() self.assertHTMLEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select name="beatle"> @@ -415,13 +386,6 @@ class FormsWidgetTestCase(TestCase): <option value="2">Yes</option> <option value="3" selected="selected">No</option> </select>""") - self.assertTrue(w._has_changed(False, None)) - self.assertTrue(w._has_changed(None, False)) - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed(False, False)) - self.assertTrue(w._has_changed(True, False)) - self.assertTrue(w._has_changed(True, None)) - self.assertTrue(w._has_changed(True, False)) def test_selectmultiple(self): w = SelectMultiple() @@ -535,14 +499,6 @@ class FormsWidgetTestCase(TestCase): # Unicode choices are correctly rendered as HTML self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '<select multiple="multiple" name="nums">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>') - # Test the usage of _has_changed - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed([], None)) - self.assertTrue(w._has_changed(None, ['1'])) - self.assertFalse(w._has_changed([1, 2], ['1', '2'])) - self.assertTrue(w._has_changed([1, 2], ['1'])) - self.assertTrue(w._has_changed([1, 2], ['1', '3'])) - # Choices can be nested one level in order to create HTML optgroups: w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) self.assertHTMLEqual(w.render('nestchoice', None), """<select multiple="multiple" name="nestchoice"> @@ -844,15 +800,6 @@ beatle J R Ringo False""") <li><label><input type="checkbox" name="escape" value="good" /> you > me</label></li> </ul>""") - # Test the usage of _has_changed - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed([], None)) - self.assertTrue(w._has_changed(None, ['1'])) - self.assertFalse(w._has_changed([1, 2], ['1', '2'])) - self.assertTrue(w._has_changed([1, 2], ['1'])) - self.assertTrue(w._has_changed([1, 2], ['1', '3'])) - self.assertFalse(w._has_changed([2, 1], ['1', '2'])) - # Unicode choices are correctly rendered as HTML self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>') @@ -886,21 +833,6 @@ beatle J R Ringo False""") w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'})), attrs={'id': 'bar'}) self.assertHTMLEqual(w.render('name', ['john', 'lennon']), '<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />') - w = MyMultiWidget(widgets=(TextInput(), TextInput())) - - # test with no initial data - self.assertTrue(w._has_changed(None, ['john', 'lennon'])) - - # test when the data is the same as initial - self.assertFalse(w._has_changed('john__lennon', ['john', 'lennon'])) - - # test when the first widget's data has changed - self.assertTrue(w._has_changed('john__lennon', ['alfred', 'lennon'])) - - # test when the last widget's data has changed. this ensures that it is not - # short circuiting while testing the widgets. - self.assertTrue(w._has_changed('john__lennon', ['john', 'denver'])) - def test_splitdatetime(self): w = SplitDateTimeWidget() self.assertHTMLEqual(w.render('date', ''), '<input type="text" name="date_0" /><input type="text" name="date_1" />') @@ -916,10 +848,6 @@ beatle J R Ringo False""") w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M') self.assertHTMLEqual(w.render('date', datetime.datetime(2006, 1, 10, 7, 30)), '<input type="text" name="date_0" value="10/01/2006" /><input type="text" name="date_1" value="07:30" />') - self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['2008-05-06', '12:40:00'])) - self.assertFalse(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:40'])) - self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:41'])) - def test_datetimeinput(self): w = DateTimeInput() self.assertHTMLEqual(w.render('date', None), '<input type="text" name="date" />') @@ -934,13 +862,6 @@ beatle J R Ringo False""") # Use 'format' to change the way a value is displayed. w = DateTimeInput(format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'}) self.assertHTMLEqual(w.render('date', d), '<input type="datetime" name="date" value="17/09/2007 12:51" />') - self.assertFalse(w._has_changed(d, '17/09/2007 12:51')) - - # Make sure a custom format works with _has_changed. The hidden input will use - data = datetime.datetime(2010, 3, 6, 12, 0, 0) - custom_format = '%d.%m.%Y %H:%M' - w = DateTimeInput(format=custom_format) - self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) def test_dateinput(self): w = DateInput() @@ -957,13 +878,6 @@ beatle J R Ringo False""") # Use 'format' to change the way a value is displayed. w = DateInput(format='%d/%m/%Y', attrs={'type': 'date'}) self.assertHTMLEqual(w.render('date', d), '<input type="date" name="date" value="17/09/2007" />') - self.assertFalse(w._has_changed(d, '17/09/2007')) - - # Make sure a custom format works with _has_changed. The hidden input will use - data = datetime.date(2010, 3, 6) - custom_format = '%d.%m.%Y' - w = DateInput(format=custom_format) - self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) def test_timeinput(self): w = TimeInput() @@ -982,13 +896,6 @@ beatle J R Ringo False""") # Use 'format' to change the way a value is displayed. w = TimeInput(format='%H:%M', attrs={'type': 'time'}) self.assertHTMLEqual(w.render('time', t), '<input type="time" name="time" value="12:51" />') - self.assertFalse(w._has_changed(t, '12:51')) - - # Make sure a custom format works with _has_changed. The hidden input will use - data = datetime.time(13, 0) - custom_format = '%I:%M %p' - w = TimeInput(format=custom_format) - self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) def test_splithiddendatetime(self): from django.forms.widgets import SplitHiddenDateTimeWidget @@ -1016,10 +923,6 @@ class FormsI18NWidgetsTestCase(TestCase): deactivate() super(FormsI18NWidgetsTestCase, self).tearDown() - def test_splitdatetime(self): - w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M') - self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06.05.2008', '12:41'])) - def test_datetimeinput(self): w = DateTimeInput() d = datetime.datetime(2007, 9, 17, 12, 51, 34, 482548) |
