Ticket #23: form-warnings.3.diff

File form-warnings.3.diff, 14.6 KB (added by jgeskens, 2 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 
    99
    1010from django.core.exceptions import ValidationError
    1111from django.forms.fields import Field, FileField
    12 from django.forms.util import flatatt, ErrorDict, ErrorList
     12from django.forms.util import flatatt, ErrorDict, ErrorList, WarningDict, WarningList
    1313from django.forms.widgets import Media, media_property, TextInput, Textarea
    1414from django.utils.datastructures import SortedDict
    1515from django.utils.html import conditional_escape, format_html
    class BaseForm(object): 
    7777    # information. Any improvements to the form API should be made to *this*
    7878    # class, not to the Form class.
    7979    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):
    8282        self.is_bound = data is not None or files is not None
    8383        self.data = data or {}
    8484        self.files = files or {}
    class BaseForm(object): 
    8686        self.prefix = prefix
    8787        self.initial = initial or {}
    8888        self.error_class = error_class
     89        self.warning_class = warning_class
    8990        self.label_suffix = label_suffix
    9091        self.empty_permitted = empty_permitted
    9192        self._errors = None # Stores the errors after clean() has been called.
     93        self._warnings = None
    9294        self._changed_data = None
    9395
    9496        # The base_fields class attribute is the *class-wide* definition of
    class BaseForm(object): 
    120122            self.full_clean()
    121123        return self._errors
    122124
    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):
    124133        """
    125134        Returns True if the form has no errors. Otherwise, False. If errors are
    126135        being ignored, returns False.
    127136        """
     137        if require_no_warnings:
     138            return self.is_bound and not bool(self.errors) and not bool(self.warnings)
    128139        return self.is_bound and not bool(self.errors)
    129140
    130141    def add_prefix(self, field_name):
    class BaseForm(object): 
    142153        """
    143154        return 'initial-%s' % self.add_prefix(field_name)
    144155
     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
    145162    def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
    146163        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
    147164        top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
     165        top_warnings = self.non_field_warnings()
    148166        output, hidden_fields = [], []
    149167
    150168        for name, field in self.fields.items():
    151169            html_class_attr = ''
    152170            bf = self[name]
     171
    153172            # Escape and cache in local variable.
    154173            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
    155176            if bf.is_hidden:
    156177                if bf_errors:
    157178                    top_errors.extend(
    class BaseForm(object): 
    168189                if errors_on_separate_row and bf_errors:
    169190                    output.append(error_row % force_text(bf_errors))
    170191
     192                if errors_on_separate_row and bf_warnings:
     193                    output.append(error_row % force_text(bf_warnings))
     194
    171195                if bf.label:
    172196                    label = conditional_escape(force_text(bf.label))
    173197                    # Only add the suffix if the label does not end in
    class BaseForm(object): 
    195219        if top_errors:
    196220            output.insert(0, error_row % force_text(top_errors))
    197221
     222        if top_warnings:
     223            output.insert(0, error_row % force_text(top_warnings))
     224
    198225        if hidden_fields: # Insert any hidden fields in the last row.
    199226            str_hidden = ''.join(hidden_fields)
    200227            if output:
    class BaseForm(object): 
    252279        """
    253280        return self.errors.get(NON_FIELD_ERRORS, self.error_class())
    254281
     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
    255290    def _raw_value(self, fieldname):
    256291        """
    257292        Returns the raw_value for a particular field name. This is just a
    class BaseForm(object): 
    263298
    264299    def full_clean(self):
    265300        """
    266         Cleans all of self.data and populates self._errors and
     301        Cleans all of self.data and populates self._errors, self._warnings and
    267302        self.cleaned_data.
    268303        """
    269304        self._errors = ErrorDict()
     305        self._warnings = WarningDict()
    270306        if not self.is_bound: # Stop further processing.
    271307            return
    272308        self.cleaned_data = {}
    class BaseForm(object): 
    287323            try:
    288324                if isinstance(field, FileField):
    289325                    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))
    291330                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))
    293335                self.cleaned_data[name] = value
    294336                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)()
    296341                    self.cleaned_data[name] = value
    297342            except ValidationError as e:
    298343                self._errors[name] = self.error_class(e.messages)
    class BaseForm(object): 
    301346
    302347    def _clean_form(self):
    303348        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()
    305353        except ValidationError as e:
    306354            self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
    307355
    class BoundField(object): 
    450498        """
    451499        return self.form.errors.get(self.name, self.form.error_class())
    452500
     501    def _warnings(self):
     502        return self.form.warnings.get(self.name, self.form.warning_class())
     503    warnings = property(_warnings)
     504
    453505    def as_widget(self, widget=None, attrs=None, only_initial=False):
    454506        """
    455507        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 
    1111from django.forms.fields import Field, ChoiceField
    1212from django.forms.forms import BaseForm, get_declared_fields
    1313from django.forms.formsets import BaseFormSet, formset_factory
    14 from django.forms.util import ErrorList
     14from django.forms.util import ErrorList, WarningList
    1515from django.forms.widgets import (SelectMultiple, HiddenInput,
    1616    MultipleHiddenInput, media_property)
    1717from django.utils.encoding import smart_text, force_text
    class ModelFormMetaclass(type): 
    264264
    265265class BaseModelForm(BaseForm):
    266266    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):
    269269        opts = self._meta
    270270        if opts.model is None:
    271271            raise ValueError('ModelForm has no model class specified.')
    class BaseModelForm(BaseForm): 
    284284        # super will stop validate_unique from being called.
    285285        self._validate_unique = False
    286286        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)
    288289
    289290    def _update_errors(self, message_dict):
    290291        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): 
    6868    def __repr__(self):
    6969        return repr([force_text(e) for e in self])
    7070
     71class 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
     85class 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
    7196# Utilities for time zone support in DateTimeField et al.
    7297
    7398def 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
    - +  
     1from __future__ import absolute_import
     2
     3from .error_messages import (FormsErrorMessagesTestCase,
     4    ModelChoiceFieldErrorMessagesTestCase)
     5from .warning_messages import WarningTests
     6from .extra import FormsExtraTestCase, FormsExtraL10NTestCase
     7from .fields import FieldsTests
     8from .forms import FormsTestCase
     9from .formsets import (FormsFormsetTestCase, FormsetAsFooTests,
     10    TestIsBoundBehavior, TestEmptyFormSet)
     11from .input_formats import (LocalizedTimeTests, CustomTimeInputFormatsTests,
     12    SimpleTimeFormatTests, LocalizedDateTests, CustomDateInputFormatsTests,
     13    SimpleDateFormatTests, LocalizedDateTimeTests,
     14    CustomDateTimeInputFormatsTests, SimpleDateTimeFormatTests)
     15from .media import FormsMediaTestCase, StaticFormsMediaTestCase
     16from .models import (TestTicket12510, ModelFormCallableModelDefault,
     17    FormsModelTestCase, RelatedModelFormTests)
     18from .regressions import FormsRegressionsTestCase
     19from .util import FormsUtilTestCase
     20from .validators import TestFieldWithValidators
     21from .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 -*-
     2from __future__ import absolute_import, unicode_literals
     3
     4from django import forms
     5from django.test import TestCase
     6
     7
     8class 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
     20class 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&#39;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        """)
Back to Top