Ticket #23: form-warnings.diff

File form-warnings.diff, 13.1 KB (added by Alex Gaynor, 16 years ago)

some initial work

  • new file django/core/warnings.py

    diff --git a/django/core/warnings.py b/django/core/warnings.py
    new file mode 100644
    index 0000000..ba0575d
    - +  
     1"""
     2A system for implementing warnings.  Based on the Django signal dispatcher.
     3"""
     4try:
     5    from threading import currentThread
     6except ImportError:
     7    from django.util.thread_support import currentThread
     8
     9from django.dispatch import Signal
     10
     11def create_reciever(callback=lambda *args, **kwargs: None, dispatch_uid=None):
     12    def method(thread_id, message, **kwargs):       
     13        if thread_id != id(currentThread()):
     14            return
     15        callback(message, **kwargs)
     16   
     17    warning.connect(method, weak=False, dispatch_uid=dispatch_uid)
     18
     19def remove_reciever(dispatch_uid=None):
     20    warning.disconnect(dispatch_uid)
     21
     22def warn(message):
     23    warning.send(None, **{'thread_id': id(currentThread()), 'message':message})
     24
     25warning = Signal(providing_args=['thread_id', 'message'])
  • django/forms/forms.py

    diff --git a/django/forms/forms.py b/django/forms/forms.py
    index f5bea10..ef50c1e 100644
    a b Form classes  
    33"""
    44
    55from copy import deepcopy
     6from random import randrange
    67
     8from django.core import warnings
    79from django.utils.datastructures import SortedDict
    810from django.utils.html import conditional_escape
    911from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
    from django.utils.safestring import mark_safe  
    1113
    1214from fields import Field, FileField
    1315from widgets import Media, media_property, TextInput, Textarea
    14 from util import flatatt, ErrorDict, ErrorList, ValidationError
     16from util import flatatt, ErrorDict, ErrorList, ValidationError, WarningDict, WarningList
    1517
    1618__all__ = ('BaseForm', 'Form')
    1719
    class BaseForm(StrAndUnicode):  
    6971    # information. Any improvements to the form API should be made to *this*
    7072    # class, not to the Form class.
    7173    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    72                  initial=None, error_class=ErrorList, label_suffix=':',
     74                 initial=None, error_class=ErrorList, warning_class=WarningList, label_suffix=':',
    7375                 empty_permitted=False):
    7476        self.is_bound = data is not None or files is not None
    7577        self.data = data or {}
    class BaseForm(StrAndUnicode):  
    7880        self.prefix = prefix
    7981        self.initial = initial or {}
    8082        self.error_class = error_class
     83        self.warning_class = warning_class
    8184        self.label_suffix = label_suffix
    8285        self.empty_permitted = empty_permitted
    8386        self._errors = None # Stores the errors after clean() has been called.
     87        self._warnings = None
    8488        self._changed_data = None
    8589
    8690        # The base_fields class attribute is the *class-wide* definition of
    class BaseForm(StrAndUnicode):  
    111115            self.full_clean()
    112116        return self._errors
    113117    errors = property(_get_errors)
     118   
     119    def _get_warnings(self):
     120        if self._warnings is None:
     121            self.full_clean()
     122        return self._warnings
     123    warnings = property(_get_warnings)
    114124
    115     def is_valid(self):
     125    def is_valid(self, require_no_warnings=False):
    116126        """
    117127        Returns True if the form has no errors. Otherwise, False. If errors are
    118128        being ignored, returns False.
    119129        """
     130        if require_no_warnings:
     131            return self.is_bound and not bool(self.errors) and not bool(self.warnings)
    120132        return self.is_bound and not bool(self.errors)
    121133
    122134    def add_prefix(self, field_name):
    class BaseForm(StrAndUnicode):  
    137149    def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
    138150        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
    139151        top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
     152        top_warnings = self.non_field_warnings()
    140153        output, hidden_fields = [], []
    141154        for name, field in self.fields.items():
    142155            bf = BoundField(self, field, name)
    143156            bf_errors = self.error_class([conditional_escape(error) for error in bf.errors]) # Escape and cache in local variable.
     157            bf_warnings = self.warning_class([conditional_escape(warning) for warning in bf.warnings])
    144158            if bf.is_hidden:
    145159                if bf_errors:
    146160                    top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
     161                if bf_warnings:
     162                    top_warnings.extend([u'Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_warnings])
    147163                hidden_fields.append(unicode(bf))
    148164            else:
    149165                if errors_on_separate_row and bf_errors:
    150166                    output.append(error_row % force_unicode(bf_errors))
     167                if errors_on_separate_row and bf_warnings:
     168                    output.append(error_row % force_unicode(bf_warnings))
    151169                if bf.label:
    152170                    label = conditional_escape(force_unicode(bf.label))
    153171                    # Only add the suffix if the label does not end in
    class BaseForm(StrAndUnicode):  
    163181                else:
    164182                    help_text = u''
    165183                output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text})
     184        if top_warnings:
     185            output.insert(0, error_row % force_unicode(top_warnings))
    166186        if top_errors:
    167187            output.insert(0, error_row % force_unicode(top_errors))
    168188        if hidden_fields: # Insert any hidden fields in the last row.
    class BaseForm(StrAndUnicode):  
    204224        are none.
    205225        """
    206226        return self.errors.get(NON_FIELD_ERRORS, self.error_class())
     227   
     228    def non_field_warnings(self):
     229        return self.warnings.get(NON_FIELD_ERRORS, self.warning_class())
    207230
    208231    def full_clean(self):
    209232        """
    class BaseForm(StrAndUnicode):  
    211234        self.cleaned_data.
    212235        """
    213236        self._errors = ErrorDict()
     237        self._warnings = WarningDict()
    214238        if not self.is_bound: # Stop further processing.
    215239            return
    216240        self.cleaned_data = {}
    class BaseForm(StrAndUnicode):  
    223247            # Each widget type knows how to retrieve its own data, because some
    224248            # widgets split data over several HTML fields.
    225249            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
     250            def add_warning(message, **kwargs):
     251                if name in self._warnings:
     252                    self._warnings[name].append(message)
     253                else:
     254                    self._warnings[name] = WarningList([message])
     255            dispatch_uid = 'warning-%s-%s' % (name ,''.join([str(randrange(0, 10)) for x in xrange(10)]))
     256            warnings.create_reciever(add_warning, dispatch_uid)
    226257            try:
    227258                if isinstance(field, FileField):
    228259                    initial = self.initial.get(name, field.initial)
    class BaseForm(StrAndUnicode):  
    237268                self._errors[name] = e.messages
    238269                if name in self.cleaned_data:
    239270                    del self.cleaned_data[name]
     271            finally:
     272                warnings.remove_reciever(dispatch_uid)
     273        def add_warning(message, **kwargs):
     274            if NON_FIELD_ERRORS in self._warnings:
     275                self._warnings[NON_FIELD_ERRORS].append(message)
     276            else:
     277                self._warnings[NON_FIELD_ERRORS] = WarningList([message])
     278        dispatch_uid = 'warning-%s-%s' % (NON_FIELD_ERRORS ,''.join([str(randrange(0, 10)) for x in xrange(10)]))
     279        warnings.create_reciever(add_warning, dispatch_uid)
    240280        try:
    241281            self.cleaned_data = self.clean()
    242282        except ValidationError, e:
    243283            self._errors[NON_FIELD_ERRORS] = e.messages
     284        finally:
     285            warnings.remove_reciever(dispatch_uid)
    244286        if self._errors:
    245287            delattr(self, 'cleaned_data')
    246288
    class BoundField(StrAndUnicode):  
    353395        """
    354396        return self.form.errors.get(self.name, self.form.error_class())
    355397    errors = property(_errors)
     398   
     399    def _warnings(self):
     400        return self.form.warnings.get(self.name, self.form.warning_class())
     401    warnings = property(_warnings)
    356402
    357403    def as_widget(self, widget=None, attrs=None, only_initial=False):
    358404        """
  • django/forms/util.py

    diff --git a/django/forms/util.py b/django/forms/util.py
    index b9b88a6..e51d810 100644
    a b class ValidationError(Exception):  
    6666        # AttributeError: ValidationError instance has no attribute 'args'
    6767        # See http://www.python.org/doc/current/tut/node10.html#handling
    6868        return repr(self.messages)
     69
     70class WarningDict(ErrorDict):
     71    """
     72    A collection of warnings that knows how to display itself in various formats.
     73
     74    The dictionary keys are the field names, and the values are the warnings.
     75    """
     76    def as_ul(self):
     77        if not self:
     78            return u''
     79        return mark_safe(u'<ul class="warninglist">%s</ul>' %
     80            ''.join([u'<li>%s%s</li>' % (k, force_unicode(v))
     81            for k, v in self.items()]))
     82
     83
     84class WarningList(ErrorList):
     85    """
     86    A collection of warnings that knows how to display itself in various formats.
     87    """
     88    def as_ul(self):
     89        if not self:
     90            return u''
     91        return mark_safe(u'<ul class="warninglist">%s</ul>' %
     92            ''.join([u'<li>%s</li>' % conditional_escape(force_unicode(e))
     93            for e in self]))
  • docs/ref/forms/validation.txt

    diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt
    index 3ad9ce9..dee7b54 100644
    a b dictionary if there are any errors in the form. However, this behaviour may  
    303303change in the future, so it's not a bad idea to clean up after yourself in the
    304304first place.
    305305
     306
     307Validation Warnings
     308===================
     309
     310In addition to raising ValidationErrors you can also use Validation Warnings. 
     311These are things you want to bring to the user's attention, but they don't
     312actually indicate that the input is invalid.
     313
     314This functionality works with ``django.core.warnings``, in a form validation
     315method when you want to throw a warning you can call ``django.core.warnings.warn``
     316with the warning message as the sole argument.
     317
     318For example, if we have a form with an ``IntegerField``, and we want to remind
     319the user that the value may be too small, but still allow it we could right a
     320validation method that looks like::
     321
     322    from django.core.warnings import warn
     323    def clean_value(self):
     324        p = self.cleaned_data['value']
     325        if p < 10:
     326            warn("That's a pretty small value.")
     327        return p
     328
     329If a user enters a value under 10 they will be warned, but by default the
     330form will still be valid.  Since there are cases where you might want a warning
     331to invalidate a form a form's ``is_valid()`` method takes a single boolean
     332argument that determines whether or not warnings should impact whether or not a
     333form is valid, by default it is False.  This allows you to have behavior such as
     334displaying a form with warnings once, but when it's resubmitted allowing any
     335warnings.
  • tests/regressiontests/forms/tests.py

    diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
    index 6a8b017..dd70b0f 100644
    a b from util import tests as util_tests  
    3030from widgets import tests as widgets_tests
    3131from formsets import tests as formset_tests
    3232from media import media_tests
     33from warnings import tests as warning_tests
    3334
    3435__test__ = {
    3536    'extra_tests': extra_tests,
    __test__ = {  
    6364    'media_tests': media_tests,
    6465    'util_tests': util_tests,
    6566    'widgets_tests': widgets_tests,
     67    'warning_tests': warning_tests,
    6668}
    6769
    6870if __name__ == "__main__":
  • new file tests/regressiontests/forms/warnings.py

    diff --git a/tests/regressiontests/forms/warnings.py b/tests/regressiontests/forms/warnings.py
    new file mode 100644
    index 0000000..92b4f28
    - +  
     1tests = """
     2>>> from django import forms
     3>>> from django.core.warnings import warn
     4>>> class PriceForm(forms.Form):
     5...     price = forms.IntegerField()
     6...     
     7...     def clean_price(self):
     8...         p = self.cleaned_data['price']
     9...         if p < 10:
     10...             warn(u"That's an awfully low price")
     11...         if p == 7:
     12...             warn(u"7 is a pretty strange price")
     13...         return p
     14
     15>>> form = PriceForm({'price': 5})
     16>>> form.is_valid()
     17True
     18>>> form.is_valid(True)
     19False
     20>>> form.warnings
     21{'price': [u"That's an awfully low price"]}
     22>>> form = PriceForm({'price': 7})
     23>>> form.is_valid()
     24True
     25>>> form.warnings
     26{'price': [u"That's an awfully low price", u'7 is a pretty strange price']}
     27>>> print form.as_p()
     28<ul class="warninglist"><li>That&#39;s an awfully low price</li><li>7 is a pretty strange price</li></ul>
     29<p><label for="id_price">Price:</label> <input type="text" name="price" value="7" id="id_price" /></p>
     30"""
Back to Top