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/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -9,7 +9,7 @@ import warnings
 
 from django.core.exceptions import ValidationError
 from django.forms.fields import Field, FileField
-from django.forms.util import flatatt, ErrorDict, ErrorList
+from django.forms.util import flatatt, ErrorDict, ErrorList, WarningDict, WarningList
 from django.forms.widgets import Media, media_property, TextInput, Textarea
 from django.utils.datastructures import SortedDict
 from django.utils.html import conditional_escape, format_html
@@ -77,8 +77,8 @@ class BaseForm(object):
     # information. Any improvements to the form API should be made to *this*
     # class, not to the Form class.
     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
-                 initial=None, error_class=ErrorList, label_suffix=':',
-                 empty_permitted=False):
+                 initial=None, error_class=ErrorList, warning_class=WarningList,
+                 label_suffix=':', empty_permitted=False):
         self.is_bound = data is not None or files is not None
         self.data = data or {}
         self.files = files or {}
@@ -86,9 +86,11 @@ class BaseForm(object):
         self.prefix = prefix
         self.initial = initial or {}
         self.error_class = error_class
+        self.warning_class = warning_class
         self.label_suffix = label_suffix
         self.empty_permitted = empty_permitted
         self._errors = None # Stores the errors after clean() has been called.
+        self._warnings = None
         self._changed_data = None
 
         # The base_fields class attribute is the *class-wide* definition of
@@ -120,11 +122,20 @@ class BaseForm(object):
             self.full_clean()
         return self._errors
 
-    def is_valid(self):
+    def _get_warnings(self):
+        "Returns a WarningDict for the data provided for the form"
+        if self._warnings is None:
+            self.full_clean()
+        return self._warnings
+    warnings = property(_get_warnings)
+
+    def is_valid(self, require_no_warnings=False):
         """
         Returns True if the form has no errors. Otherwise, False. If errors are
         being ignored, returns False.
         """
+        if require_no_warnings:
+            return self.is_bound and not bool(self.errors) and not bool(self.warnings)
         return self.is_bound and not bool(self.errors)
 
     def add_prefix(self, field_name):
@@ -142,16 +153,26 @@ class BaseForm(object):
         """
         return 'initial-%s' % self.add_prefix(field_name)
 
+    def add_warning(self, field_name, message):
+        if field_name in self._warnings:
+            self._warnings[field_name].append(message)
+        else:
+            self._warnings[field_name] = WarningList([message])
+
     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.
+        top_warnings = self.non_field_warnings()
         output, hidden_fields = [], []
 
         for name, field in self.fields.items():
             html_class_attr = ''
             bf = self[name]
+
             # Escape and cache in local variable.
             bf_errors = self.error_class([conditional_escape(error) for error in bf.errors])
+            bf_warnings = self.warning_class([conditional_escape(warning) for warning in bf.warnings])
+
             if bf.is_hidden:
                 if bf_errors:
                     top_errors.extend(
@@ -168,6 +189,9 @@ class BaseForm(object):
                 if errors_on_separate_row and bf_errors:
                     output.append(error_row % force_text(bf_errors))
 
+                if errors_on_separate_row and bf_warnings:
+                    output.append(error_row % force_text(bf_warnings))
+
                 if bf.label:
                     label = conditional_escape(force_text(bf.label))
                     # Only add the suffix if the label does not end in
@@ -195,6 +219,9 @@ class BaseForm(object):
         if top_errors:
             output.insert(0, error_row % force_text(top_errors))
 
+        if top_warnings:
+            output.insert(0, error_row % force_text(top_warnings))
+
         if hidden_fields: # Insert any hidden fields in the last row.
             str_hidden = ''.join(hidden_fields)
             if output:
@@ -252,6 +279,14 @@ class BaseForm(object):
         """
         return self.errors.get(NON_FIELD_ERRORS, self.error_class())
 
+    def non_field_warnings(self):
+        """
+        Returns an WarningList of warnings that aren't associated with a particular
+        field -- i.e., from Form.clean(). Returns an empty WarningList if there
+        are none.
+        """
+        return self.warnings.get(NON_FIELD_ERRORS, self.warning_class())
+
     def _raw_value(self, fieldname):
         """
         Returns the raw_value for a particular field name. This is just a
@@ -263,10 +298,11 @@ class BaseForm(object):
 
     def full_clean(self):
         """
-        Cleans all of self.data and populates self._errors and
+        Cleans all of self.data and populates self._errors, self._warnings and
         self.cleaned_data.
         """
         self._errors = ErrorDict()
+        self._warnings = WarningDict()
         if not self.is_bound: # Stop further processing.
             return
         self.cleaned_data = {}
@@ -287,12 +323,21 @@ class BaseForm(object):
             try:
                 if isinstance(field, FileField):
                     initial = self.initial.get(name, field.initial)
-                    value = field.clean(value, initial)
+                    try:
+                        value = field.clean(value, initial)
+                    except TypeError:
+                        value = field.clean(value, initial, lambda m: self.add_warning(name, m))
                 else:
-                    value = field.clean(value)
+                    try:
+                        value = field.clean(value)
+                    except TypeError:
+                        value = field.clean(value, lambda m: self.add_warning(name, m))
                 self.cleaned_data[name] = value
                 if hasattr(self, 'clean_%s' % name):
-                    value = getattr(self, 'clean_%s' % name)()
+                    try:
+                        value = getattr(self, 'clean_%s' % name)(lambda m: self.add_warning(name, m))
+                    except TypeError:
+                        value = getattr(self, 'clean_%s' % name)()
                     self.cleaned_data[name] = value
             except ValidationError as e:
                 self._errors[name] = self.error_class(e.messages)
@@ -301,7 +346,10 @@ class BaseForm(object):
 
     def _clean_form(self):
         try:
-            self.cleaned_data = self.clean()
+            try:
+                self.cleaned_data = self.clean(lambda m: self.add_warning(name, m))
+            except TypeError:
+                self.cleaned_data = self.clean()
         except ValidationError as e:
             self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
 
@@ -450,6 +498,10 @@ class BoundField(object):
         """
         return self.form.errors.get(self.name, self.form.error_class())
 
+    def _warnings(self):
+        return self.form.warnings.get(self.name, self.form.warning_class())
+    warnings = property(_warnings)
+
     def as_widget(self, widget=None, attrs=None, only_initial=False):
         """
         Renders the field by rendering the passed widget, adding any HTML
diff --git a/django/forms/models.py b/django/forms/models.py
index af5cda8..71e8a14 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -11,7 +11,7 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
 from django.forms.fields import Field, ChoiceField
 from django.forms.forms import BaseForm, get_declared_fields
 from django.forms.formsets import BaseFormSet, formset_factory
-from django.forms.util import ErrorList
+from django.forms.util import ErrorList, WarningList
 from django.forms.widgets import (SelectMultiple, HiddenInput,
     MultipleHiddenInput, media_property)
 from django.utils.encoding import smart_text, force_text
@@ -264,8 +264,8 @@ class ModelFormMetaclass(type):
 
 class BaseModelForm(BaseForm):
     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
-                 initial=None, error_class=ErrorList, label_suffix=':',
-                 empty_permitted=False, instance=None):
+                 initial=None, error_class=ErrorList, warning_class=WarningList,
+                 label_suffix=':', empty_permitted=False, instance=None):
         opts = self._meta
         if opts.model is None:
             raise ValueError('ModelForm has no model class specified.')
@@ -284,7 +284,8 @@ class BaseModelForm(BaseForm):
         # super will stop validate_unique from being called.
         self._validate_unique = False
         super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
-                                            error_class, label_suffix, empty_permitted)
+                                            error_class, warning_class, label_suffix,
+                                            empty_permitted)
 
     def _update_errors(self, message_dict):
         for k, v in message_dict.items():
diff --git a/django/forms/util.py b/django/forms/util.py
index f1b864e..3457af2 100644
--- a/django/forms/util.py
+++ b/django/forms/util.py
@@ -68,6 +68,31 @@ class ErrorList(list):
     def __repr__(self):
         return repr([force_text(e) for e in self])
 
+class WarningDict(ErrorDict):
+    """
+    A collection of warnings that knows how to display itself in various formats.
+
+    The dictionary keys are the field names, and the values are the warnings.
+    """
+    def as_ul(self):
+        if not self: return ''
+        return format_html('<ul class="warninglist">{0}</ul>',
+                           format_html_join('', '<li>{0}</li>',
+                                            ((k, force_text(v))
+                                             for k, v in self.items())
+                           ))
+
+class WarningList(ErrorList):
+    """
+    A collection of warnings that knows how to display itself in various formats.
+    """
+    def as_ul(self):
+        if not self: return ''
+        return format_html('<ul class="warninglist">{0}</ul>',
+                           format_html_join('', '<li>{0}</li>',
+                                            ((force_text(e),) for e in self)
+                           ))
+
 # Utilities for time zone support in DateTimeField et al.
 
 def from_current_timezone(value):
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
--- /dev/null
+++ b/tests/regressiontests/forms/tests/__init__.py
@@ -0,0 +1,22 @@
+from __future__ import absolute_import
+
+from .error_messages import (FormsErrorMessagesTestCase,
+    ModelChoiceFieldErrorMessagesTestCase)
+from .warning_messages import WarningTests
+from .extra import FormsExtraTestCase, FormsExtraL10NTestCase
+from .fields import FieldsTests
+from .forms import FormsTestCase
+from .formsets import (FormsFormsetTestCase, FormsetAsFooTests,
+    TestIsBoundBehavior, TestEmptyFormSet)
+from .input_formats import (LocalizedTimeTests, CustomTimeInputFormatsTests,
+    SimpleTimeFormatTests, LocalizedDateTests, CustomDateInputFormatsTests,
+    SimpleDateFormatTests, LocalizedDateTimeTests,
+    CustomDateTimeInputFormatsTests, SimpleDateTimeFormatTests)
+from .media import FormsMediaTestCase, StaticFormsMediaTestCase
+from .models import (TestTicket12510, ModelFormCallableModelDefault,
+    FormsModelTestCase, RelatedModelFormTests)
+from .regressions import FormsRegressionsTestCase
+from .util import FormsUtilTestCase
+from .validators import TestFieldWithValidators
+from .widgets import (FormsWidgetTestCase, FormsI18NWidgetsTestCase,
+    WidgetTests, LiveWidgetTests, ClearableFileInputTests)
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
--- /dev/null
+++ b/tests/regressiontests/forms/tests/warning_messages.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+from django import forms
+from django.test import TestCase
+
+
+class PriceForm(forms.Form):
+    price = forms.IntegerField()
+
+    def clean_price(self, warn):
+        p = self.cleaned_data['price']
+        if p < 10:
+            warn("That's an awfully low price")
+        if p == 7:
+            warn("7 is a pretty strange price")
+        return p
+
+
+class WarningTests(TestCase):
+    def test_warnings(self):
+        form = PriceForm({'price': 5})
+        self.assertTrue(form.is_valid())
+        self.assertFalse(form.is_valid(require_no_warnings=True))
+        self.assertEqual(form.warnings, {
+            'price': ["That's an awfully low price"]
+        })
+        form = PriceForm({'price': 7})
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.warnings, {
+            'price': ["That's an awfully low price",
+                      "7 is a pretty strange price"]
+        })
+        self.assertHTMLEqual(form.as_p(), """
+            <ul class="warninglist">
+                <li>That&#39;s an awfully low price</li>
+                <li>7 is a pretty strange price</li>
+            </ul>
+            <p>
+                <label for="id_price">Price:</label>
+                <input id="id_price" name="price" type="text" value="7" />
+            </p>
+        """)
-- 
1.7.6.1

