Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

newforms-admin: Fixed #5353. Added FormSet validation hook. Separated…

… a few things out from the original patch and added more tests. Thanks, Honza Kral.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@6419 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit b40f9b63bb6900689e6052052f620d6447ec63e5 1 parent 9687447
Joseph Kocherhans authored September 25, 2007
70  django/newforms/formsets.py
... ...
@@ -1,6 +1,7 @@
1  
-from forms import Form, ValidationError
  1
+from forms import Form
2 2
 from fields import IntegerField, BooleanField
3 3
 from widgets import HiddenInput, Media
  4
+from util import ErrorList, ValidationError
4 5
 
5 6
 __all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
6 7
 
@@ -22,13 +23,15 @@ def __init__(self, *args, **kwargs):
22 23
 class BaseFormSet(object):
23 24
     """A collection of instances of the same Form class."""
24 25
 
25  
-    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
  26
+    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
  27
+            initial=None, error_class=ErrorList):
26 28
         self.is_bound = data is not None or files is not None
27 29
         self.prefix = prefix or 'form'
28 30
         self.auto_id = auto_id
29 31
         self.data = data
30 32
         self.files = files
31 33
         self.initial = initial
  34
+        self.error_class = error_class
32 35
         # initialization is different depending on whether we recieved data, initial, or nothing
33 36
         if data or files:
34 37
             self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
@@ -92,55 +95,78 @@ def _forms(self):
92 95
         return self.change_forms + self.add_forms
93 96
     forms = property(_forms)
94 97
 
  98
+    def non_form_errors(self):
  99
+        """
  100
+        Returns an ErrorList of errors that aren't associated with a particular
  101
+        form -- i.e., from formset.clean(). Returns an empty ErrorList if there
  102
+        are none.
  103
+        """
  104
+        if hasattr(self, '_non_form_errors'):
  105
+            return self._non_form_errors
  106
+        return self.error_class()
  107
+
95 108
     def full_clean(self):
96 109
         """Cleans all of self.data and populates self.__errors and self.cleaned_data."""
97  
-        is_valid = True
  110
+        self._is_valid = True # Assume the formset is valid until proven otherwise.
98 111
         errors = []
99 112
         if not self.is_bound: # Stop further processing.
100 113
             self.__errors = errors
101 114
             return
102  
-        cleaned_data = []
103  
-        deleted_data = []
104  
-        
  115
+        self.cleaned_data = []
  116
+        self.deleted_data = []
105 117
         # Process change forms
106 118
         for form in self.change_forms:
107 119
             if form.is_valid():
108 120
                 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
109  
-                    deleted_data.append(form.cleaned_data)
  121
+                    self.deleted_data.append(form.cleaned_data)
110 122
                 else:
111  
-                    cleaned_data.append(form.cleaned_data)
  123
+                    self.cleaned_data.append(form.cleaned_data)
112 124
             else:
113  
-                is_valid = False
  125
+                self._is_valid = False
114 126
             errors.append(form.errors)
115  
-        
116 127
         # Process add forms in reverse so we can easily tell when the remaining
117 128
         # ones should be required.
118  
-        required = False
  129
+        reamining_forms_required = False
119 130
         add_errors = []
120 131
         for i in range(len(self.add_forms)-1, -1, -1):
121 132
             form = self.add_forms[i]
122 133
             # If an add form is empty, reset it so it won't have any errors
123  
-            if form.is_empty([ORDERING_FIELD_NAME]) and not required:
  134
+            if form.is_empty([ORDERING_FIELD_NAME]) and not reamining_forms_required:
124 135
                 form.reset()
125 136
                 continue
126 137
             else:
127  
-                required = True
  138
+                reamining_forms_required = True
128 139
                 if form.is_valid():
129  
-                    cleaned_data.append(form.cleaned_data)
  140
+                    self.cleaned_data.append(form.cleaned_data)
130 141
                 else:
131  
-                    is_valid = False
  142
+                    self._is_valid = False
132 143
             add_errors.append(form.errors)
133 144
         add_errors.reverse()
134 145
         errors.extend(add_errors)
135  
-
  146
+        # Sort cleaned_data if the formset is orderable.
136 147
         if self.orderable:
137  
-            cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
138  
-
139  
-        if is_valid:
140  
-            self.cleaned_data = cleaned_data
141  
-            self.deleted_data = deleted_data
  148
+            self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
  149
+        # Give self.clean() a chance to do validation
  150
+        try:
  151
+            self.cleaned_data = self.clean()
  152
+        except ValidationError, e:
  153
+            self._non_form_errors = e.messages
  154
+            self._is_valid = False
142 155
         self.errors = errors
143  
-        self._is_valid = is_valid
  156
+        # If there were errors, be consistent with forms and remove the
  157
+        # cleaned_data and deleted_data attributes.
  158
+        if not self._is_valid:
  159
+            delattr(self, 'cleaned_data')
  160
+            delattr(self, 'deleted_data')
  161
+
  162
+    def clean(self):
  163
+        """
  164
+        Hook for doing any extra formset-wide cleaning after Form.clean() has
  165
+        been called on every form. Any ValidationError raised by this method
  166
+        will not be associated with a particular form; it will be accesible
  167
+        via formset.non_form_errors()
  168
+        """
  169
+        return self.cleaned_data
144 170
 
145 171
     def add_fields(self, form, index):
146 172
         """A hook for adding extra fields on to each form instance."""
65  tests/regressiontests/forms/formsets.py
@@ -5,8 +5,8 @@
5 5
 FormSet allows us to use multiple instance of the same form on 1 page. For now,
6 6
 the best way to create a FormSet is by using the formset_for_form function.
7 7
 
8  
->>> from django.newforms import Form, CharField, IntegerField
9  
->>> from django.newforms.formsets import formset_for_form
  8
+>>> from django.newforms import Form, CharField, IntegerField, ValidationError
  9
+>>> from django.newforms.formsets import formset_for_form, BaseFormSet
10 10
 
11 11
 >>> class Choice(Form):
12 12
 ...     choice = CharField()
@@ -420,4 +420,65 @@
420 420
 [{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]
421 421
 
422 422
 
  423
+# FormSet clean hook ##########################################################
  424
+
  425
+FormSets have a hook for doing extra validation that shouldn't be tied to any
  426
+particular form. It follows the same pattern as the clean hook on Forms.
  427
+
  428
+Let's define a FormSet that takes a list of favorite drinks, but raises am
  429
+error if there are any duplicates.
  430
+
  431
+>>> class FavoriteDrinkForm(Form):
  432
+...     name = CharField()
  433
+...
  434
+
  435
+>>> class FavoriteDrinksFormSet(BaseFormSet):
  436
+...     form_class = FavoriteDrinkForm
  437
+...     num_extra = 2
  438
+...     orderable = False
  439
+...     deletable = False
  440
+...
  441
+...     def clean(self):
  442
+...         seen_drinks = []
  443
+...         for drink in self.cleaned_data:
  444
+...             if drink['name'] in seen_drinks:
  445
+...                 raise ValidationError('You may only specify a drink once.')
  446
+...             seen_drinks.append(drink['name'])
  447
+...         return self.cleaned_data
  448
+...
  449
+
  450
+We start out with a some duplicate data.
  451
+
  452
+>>> data = {
  453
+...     'drinks-COUNT': '2',
  454
+...     'drinks-0-name': 'Gin and Tonic',
  455
+...     'drinks-1-name': 'Gin and Tonic',
  456
+... }
  457
+
  458
+>>> formset = FavoriteDrinksFormSet(data, prefix='drinks')
  459
+>>> formset.is_valid()
  460
+False
  461
+
  462
+Any errors raised by formset.clean() are available via the
  463
+formset.non_form_errors() method.
  464
+
  465
+>>> for error in formset.non_form_errors():
  466
+...     print error
  467
+You may only specify a drink once.
  468
+
  469
+
  470
+Make sure we didn't break the valid case.
  471
+
  472
+>>> data = {
  473
+...     'drinks-COUNT': '2',
  474
+...     'drinks-0-name': 'Gin and Tonic',
  475
+...     'drinks-1-name': 'Bloody Mary',
  476
+... }
  477
+
  478
+>>> formset = FavoriteDrinksFormSet(data, prefix='drinks')
  479
+>>> formset.is_valid()
  480
+True
  481
+>>> for error in formset.non_form_errors():
  482
+...     print error
  483
+
423 484
 """

0 notes on commit b40f9b6

Please sign in to comment.
Something went wrong with that request. Please try again.