Ticket #23: form-warnings.3.diff
File form-warnings.3.diff, 14.6 KB (added by , 12 years ago) |
---|
-
django/forms/forms.py
From 713851dc1a84992951b8fbae5a7896df2fb96822 Mon Sep 17 00:00:00 2001 From: jgeskens <jef.geskens@gmail.com> Date: Sat, 18 May 2013 16:54:06 +0200 Subject: [PATCH] Improved patch for #23 --- django/forms/forms.py | 70 +++++++++++++++++--- django/forms/models.py | 9 ++- django/forms/util.py | 25 +++++++ tests/regressiontests/forms/tests/__init__.py | 22 ++++++ .../forms/tests/warning_messages.py | 43 ++++++++++++ 5 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 tests/regressiontests/__init__.py create mode 100644 tests/regressiontests/forms/tests/__init__.py create mode 100644 tests/regressiontests/forms/tests/warning_messages.py diff --git a/django/forms/forms.py b/django/forms/forms.py index b231de4..90a70c5 100644
a b import warnings 9 9 10 10 from django.core.exceptions import ValidationError 11 11 from django.forms.fields import Field, FileField 12 from django.forms.util import flatatt, ErrorDict, ErrorList 12 from django.forms.util import flatatt, ErrorDict, ErrorList, WarningDict, WarningList 13 13 from django.forms.widgets import Media, media_property, TextInput, Textarea 14 14 from django.utils.datastructures import SortedDict 15 15 from django.utils.html import conditional_escape, format_html … … class BaseForm(object): 77 77 # information. Any improvements to the form API should be made to *this* 78 78 # class, not to the Form class. 79 79 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 80 initial=None, error_class=ErrorList, label_suffix=':',81 empty_permitted=False):80 initial=None, error_class=ErrorList, warning_class=WarningList, 81 label_suffix=':', empty_permitted=False): 82 82 self.is_bound = data is not None or files is not None 83 83 self.data = data or {} 84 84 self.files = files or {} … … class BaseForm(object): 86 86 self.prefix = prefix 87 87 self.initial = initial or {} 88 88 self.error_class = error_class 89 self.warning_class = warning_class 89 90 self.label_suffix = label_suffix 90 91 self.empty_permitted = empty_permitted 91 92 self._errors = None # Stores the errors after clean() has been called. 93 self._warnings = None 92 94 self._changed_data = None 93 95 94 96 # The base_fields class attribute is the *class-wide* definition of … … class BaseForm(object): 120 122 self.full_clean() 121 123 return self._errors 122 124 123 def is_valid(self): 125 def _get_warnings(self): 126 "Returns a WarningDict for the data provided for the form" 127 if self._warnings is None: 128 self.full_clean() 129 return self._warnings 130 warnings = property(_get_warnings) 131 132 def is_valid(self, require_no_warnings=False): 124 133 """ 125 134 Returns True if the form has no errors. Otherwise, False. If errors are 126 135 being ignored, returns False. 127 136 """ 137 if require_no_warnings: 138 return self.is_bound and not bool(self.errors) and not bool(self.warnings) 128 139 return self.is_bound and not bool(self.errors) 129 140 130 141 def add_prefix(self, field_name): … … class BaseForm(object): 142 153 """ 143 154 return 'initial-%s' % self.add_prefix(field_name) 144 155 156 def add_warning(self, field_name, message): 157 if field_name in self._warnings: 158 self._warnings[field_name].append(message) 159 else: 160 self._warnings[field_name] = WarningList([message]) 161 145 162 def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): 146 163 "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." 147 164 top_errors = self.non_field_errors() # Errors that should be displayed above all fields. 165 top_warnings = self.non_field_warnings() 148 166 output, hidden_fields = [], [] 149 167 150 168 for name, field in self.fields.items(): 151 169 html_class_attr = '' 152 170 bf = self[name] 171 153 172 # Escape and cache in local variable. 154 173 bf_errors = self.error_class([conditional_escape(error) for error in bf.errors]) 174 bf_warnings = self.warning_class([conditional_escape(warning) for warning in bf.warnings]) 175 155 176 if bf.is_hidden: 156 177 if bf_errors: 157 178 top_errors.extend( … … class BaseForm(object): 168 189 if errors_on_separate_row and bf_errors: 169 190 output.append(error_row % force_text(bf_errors)) 170 191 192 if errors_on_separate_row and bf_warnings: 193 output.append(error_row % force_text(bf_warnings)) 194 171 195 if bf.label: 172 196 label = conditional_escape(force_text(bf.label)) 173 197 # Only add the suffix if the label does not end in … … class BaseForm(object): 195 219 if top_errors: 196 220 output.insert(0, error_row % force_text(top_errors)) 197 221 222 if top_warnings: 223 output.insert(0, error_row % force_text(top_warnings)) 224 198 225 if hidden_fields: # Insert any hidden fields in the last row. 199 226 str_hidden = ''.join(hidden_fields) 200 227 if output: … … class BaseForm(object): 252 279 """ 253 280 return self.errors.get(NON_FIELD_ERRORS, self.error_class()) 254 281 282 def non_field_warnings(self): 283 """ 284 Returns an WarningList of warnings that aren't associated with a particular 285 field -- i.e., from Form.clean(). Returns an empty WarningList if there 286 are none. 287 """ 288 return self.warnings.get(NON_FIELD_ERRORS, self.warning_class()) 289 255 290 def _raw_value(self, fieldname): 256 291 """ 257 292 Returns the raw_value for a particular field name. This is just a … … class BaseForm(object): 263 298 264 299 def full_clean(self): 265 300 """ 266 Cleans all of self.data and populates self._errors and301 Cleans all of self.data and populates self._errors, self._warnings and 267 302 self.cleaned_data. 268 303 """ 269 304 self._errors = ErrorDict() 305 self._warnings = WarningDict() 270 306 if not self.is_bound: # Stop further processing. 271 307 return 272 308 self.cleaned_data = {} … … class BaseForm(object): 287 323 try: 288 324 if isinstance(field, FileField): 289 325 initial = self.initial.get(name, field.initial) 290 value = field.clean(value, initial) 326 try: 327 value = field.clean(value, initial) 328 except TypeError: 329 value = field.clean(value, initial, lambda m: self.add_warning(name, m)) 291 330 else: 292 value = field.clean(value) 331 try: 332 value = field.clean(value) 333 except TypeError: 334 value = field.clean(value, lambda m: self.add_warning(name, m)) 293 335 self.cleaned_data[name] = value 294 336 if hasattr(self, 'clean_%s' % name): 295 value = getattr(self, 'clean_%s' % name)() 337 try: 338 value = getattr(self, 'clean_%s' % name)(lambda m: self.add_warning(name, m)) 339 except TypeError: 340 value = getattr(self, 'clean_%s' % name)() 296 341 self.cleaned_data[name] = value 297 342 except ValidationError as e: 298 343 self._errors[name] = self.error_class(e.messages) … … class BaseForm(object): 301 346 302 347 def _clean_form(self): 303 348 try: 304 self.cleaned_data = self.clean() 349 try: 350 self.cleaned_data = self.clean(lambda m: self.add_warning(name, m)) 351 except TypeError: 352 self.cleaned_data = self.clean() 305 353 except ValidationError as e: 306 354 self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages) 307 355 … … class BoundField(object): 450 498 """ 451 499 return self.form.errors.get(self.name, self.form.error_class()) 452 500 501 def _warnings(self): 502 return self.form.warnings.get(self.name, self.form.warning_class()) 503 warnings = property(_warnings) 504 453 505 def as_widget(self, widget=None, attrs=None, only_initial=False): 454 506 """ 455 507 Renders the field by rendering the passed widget, adding any HTML -
django/forms/models.py
diff --git a/django/forms/models.py b/django/forms/models.py index af5cda8..71e8a14 100644
a b from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError 11 11 from django.forms.fields import Field, ChoiceField 12 12 from django.forms.forms import BaseForm, get_declared_fields 13 13 from django.forms.formsets import BaseFormSet, formset_factory 14 from django.forms.util import ErrorList 14 from django.forms.util import ErrorList, WarningList 15 15 from django.forms.widgets import (SelectMultiple, HiddenInput, 16 16 MultipleHiddenInput, media_property) 17 17 from django.utils.encoding import smart_text, force_text … … class ModelFormMetaclass(type): 264 264 265 265 class BaseModelForm(BaseForm): 266 266 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 267 initial=None, error_class=ErrorList, label_suffix=':',268 empty_permitted=False, instance=None):267 initial=None, error_class=ErrorList, warning_class=WarningList, 268 label_suffix=':', empty_permitted=False, instance=None): 269 269 opts = self._meta 270 270 if opts.model is None: 271 271 raise ValueError('ModelForm has no model class specified.') … … class BaseModelForm(BaseForm): 284 284 # super will stop validate_unique from being called. 285 285 self._validate_unique = False 286 286 super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, 287 error_class, label_suffix, empty_permitted) 287 error_class, warning_class, label_suffix, 288 empty_permitted) 288 289 289 290 def _update_errors(self, message_dict): 290 291 for k, v in message_dict.items(): -
django/forms/util.py
diff --git a/django/forms/util.py b/django/forms/util.py index f1b864e..3457af2 100644
a b class ErrorList(list): 68 68 def __repr__(self): 69 69 return repr([force_text(e) for e in self]) 70 70 71 class WarningDict(ErrorDict): 72 """ 73 A collection of warnings that knows how to display itself in various formats. 74 75 The dictionary keys are the field names, and the values are the warnings. 76 """ 77 def as_ul(self): 78 if not self: return '' 79 return format_html('<ul class="warninglist">{0}</ul>', 80 format_html_join('', '<li>{0}</li>', 81 ((k, force_text(v)) 82 for k, v in self.items()) 83 )) 84 85 class WarningList(ErrorList): 86 """ 87 A collection of warnings that knows how to display itself in various formats. 88 """ 89 def as_ul(self): 90 if not self: return '' 91 return format_html('<ul class="warninglist">{0}</ul>', 92 format_html_join('', '<li>{0}</li>', 93 ((force_text(e),) for e in self) 94 )) 95 71 96 # Utilities for time zone support in DateTimeField et al. 72 97 73 98 def from_current_timezone(value): -
new file tests/regressiontests/forms/tests/__init__.py
diff --git a/tests/regressiontests/__init__.py b/tests/regressiontests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py new file mode 100644 index 0000000..ca6ec92
- + 1 from __future__ import absolute_import 2 3 from .error_messages import (FormsErrorMessagesTestCase, 4 ModelChoiceFieldErrorMessagesTestCase) 5 from .warning_messages import WarningTests 6 from .extra import FormsExtraTestCase, FormsExtraL10NTestCase 7 from .fields import FieldsTests 8 from .forms import FormsTestCase 9 from .formsets import (FormsFormsetTestCase, FormsetAsFooTests, 10 TestIsBoundBehavior, TestEmptyFormSet) 11 from .input_formats import (LocalizedTimeTests, CustomTimeInputFormatsTests, 12 SimpleTimeFormatTests, LocalizedDateTests, CustomDateInputFormatsTests, 13 SimpleDateFormatTests, LocalizedDateTimeTests, 14 CustomDateTimeInputFormatsTests, SimpleDateTimeFormatTests) 15 from .media import FormsMediaTestCase, StaticFormsMediaTestCase 16 from .models import (TestTicket12510, ModelFormCallableModelDefault, 17 FormsModelTestCase, RelatedModelFormTests) 18 from .regressions import FormsRegressionsTestCase 19 from .util import FormsUtilTestCase 20 from .validators import TestFieldWithValidators 21 from .widgets import (FormsWidgetTestCase, FormsI18NWidgetsTestCase, 22 WidgetTests, LiveWidgetTests, ClearableFileInputTests) -
new file tests/regressiontests/forms/tests/warning_messages.py
diff --git a/tests/regressiontests/forms/tests/warning_messages.py b/tests/regressiontests/forms/tests/warning_messages.py new file mode 100644 index 0000000..0baf03b
- + 1 # -*- coding: utf-8 -*- 2 from __future__ import absolute_import, unicode_literals 3 4 from django import forms 5 from django.test import TestCase 6 7 8 class PriceForm(forms.Form): 9 price = forms.IntegerField() 10 11 def clean_price(self, warn): 12 p = self.cleaned_data['price'] 13 if p < 10: 14 warn("That's an awfully low price") 15 if p == 7: 16 warn("7 is a pretty strange price") 17 return p 18 19 20 class WarningTests(TestCase): 21 def test_warnings(self): 22 form = PriceForm({'price': 5}) 23 self.assertTrue(form.is_valid()) 24 self.assertFalse(form.is_valid(require_no_warnings=True)) 25 self.assertEqual(form.warnings, { 26 'price': ["That's an awfully low price"] 27 }) 28 form = PriceForm({'price': 7}) 29 self.assertTrue(form.is_valid()) 30 self.assertEqual(form.warnings, { 31 'price': ["That's an awfully low price", 32 "7 is a pretty strange price"] 33 }) 34 self.assertHTMLEqual(form.as_p(), """ 35 <ul class="warninglist"> 36 <li>That's an awfully low price</li> 37 <li>7 is a pretty strange price</li> 38 </ul> 39 <p> 40 <label for="id_price">Price:</label> 41 <input id="id_price" name="price" type="text" value="7" /> 42 </p> 43 """)