Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

newforms-admin: Initial implementation of FormSet.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@4836 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit e2b49c379a56730b73d79ec05d6edc790ba08cc9 1 parent 132cf25
Joseph Kocherhans authored March 28, 2007
18  django/newforms/forms.py
@@ -159,6 +159,24 @@ def non_field_errors(self):
159 159
         """
160 160
         return self.errors.get(NON_FIELD_ERRORS, ErrorList())
161 161
 
  162
+    def is_empty(self, exceptions=None):
  163
+        """
  164
+        Returns True if this form has been bound and all fields that aren't
  165
+        listed in exceptions are empty.
  166
+        """
  167
+        # TODO: This could probably use some optimization
  168
+        exceptions = exceptions or []
  169
+        for name, field in self.fields.items():
  170
+            if name in exceptions:
  171
+                continue
  172
+            # value_from_datadict() gets the data from the dictionary.
  173
+            # Each widget type knows how to retrieve its own data, because some
  174
+            # widgets split data over several HTML fields.
  175
+            value = field.widget.value_from_datadict(self.data, self.add_prefix(name))
  176
+            if value not in (None, ''):
  177
+                return False
  178
+        return True
  179
+
162 180
     def full_clean(self):
163 181
         """
164 182
         Cleans all of self.data and populates self.__errors and self.clean_data.
154  django/newforms/formsets.py
... ...
@@ -0,0 +1,154 @@
  1
+from django import newforms as forms
  2
+
  3
+# special field names
  4
+FORM_COUNT_FIELD_NAME = 'COUNT'
  5
+ORDERING_FIELD_NAME = 'ORDER'
  6
+DELETION_FIELD_NAME = 'DELETE'
  7
+
  8
+class ManagementForm(forms.Form):
  9
+    """
  10
+    ``ManagementForm`` is used to keep track of how many form instances
  11
+    are displayed on the page. If adding new forms via javascript, you should
  12
+    increment the count field of this form as well.
  13
+    """
  14
+    def __init__(self, *args, **kwargs):
  15
+        self.base_fields[FORM_COUNT_FIELD_NAME] = forms.IntegerField(widget=forms.HiddenInput)
  16
+        super(ManagementForm, self).__init__(*args, **kwargs)
  17
+
  18
+class FormSet(object):
  19
+    """A collection of instances of the same Form class."""
  20
+
  21
+    def __init__(self, form_class, data=None, auto_id='id_%s', prefix=None, initial=None):
  22
+        self.form_class = form_class
  23
+        self.prefix = prefix or 'form'
  24
+        self.auto_id = auto_id
  25
+        # initialization is different depending on whether we recieved data, initial, or nothing
  26
+        if data:
  27
+            self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
  28
+            if self.management_form.is_valid():
  29
+                form_count = self.management_form.clean_data[FORM_COUNT_FIELD_NAME]
  30
+            else:
  31
+                # not sure that ValidationError is the best thing to raise here
  32
+                raise forms.ValidationError('ManagementForm data is missing or has been tampered with')
  33
+            self.form_list = self._forms_for_data(data, form_count=form_count)
  34
+        elif initial:
  35
+            form_count = len(initial)
  36
+            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: form_count+1}, auto_id=self.auto_id, prefix=self.prefix)
  37
+            self.form_list = self._forms_for_initial(initial, form_count=form_count)
  38
+        else:
  39
+            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: 1}, auto_id=self.auto_id, prefix=self.prefix)
  40
+            self.form_list = self._empty_forms(form_count=1)
  41
+
  42
+    # TODO: initialization needs some cleanup and some restructuring
  43
+    # TODO: allow more than 1 extra blank form to be displayed
  44
+
  45
+    def _forms_for_data(self, data, form_count):
  46
+        form_list = []
  47
+        for i in range(0, form_count-1):
  48
+            form_instance = self.form_class(data, auto_id=self.auto_id, prefix=self.add_prefix(i))
  49
+            self.add_fields(form_instance, i)
  50
+            form_list.append(form_instance)
  51
+        # hackish, but if the last form stayed empty, replace it with a 
  52
+        # blank one. no 'data' or 'initial' arguments
  53
+        form_instance = self.form_class(data, auto_id=self.auto_id, prefix=self.add_prefix(form_count-1))
  54
+        if form_instance.is_empty():
  55
+            form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(form_count-1))
  56
+        self.add_fields(form_instance, form_count-1)
  57
+        form_list.append(form_instance)
  58
+        return form_list
  59
+
  60
+    def _forms_for_initial(self, initial, form_count):
  61
+        form_list = []
  62
+        # generate a form for each item in initial, plus one empty one
  63
+        for i in range(0, form_count):
  64
+            form_instance = self.form_class(initial=initial[i], auto_id=self.auto_id, prefix=self.add_prefix(i))
  65
+            self.add_fields(form_instance, i)
  66
+            form_list.append(form_instance)
  67
+        # add 1 empty form
  68
+        form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i+1))
  69
+        self.add_fields(form_instance, i+1)
  70
+        form_list.append(form_instance)
  71
+        return form_list
  72
+
  73
+    def _empty_forms(self, form_count):
  74
+        form_list = []
  75
+        # we only need one form, there's no inital data and no post data
  76
+        form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(0))
  77
+        form_list.append(form_instance)
  78
+        return form_list
  79
+
  80
+    def get_forms(self):
  81
+        return self.form_list
  82
+
  83
+    def add_fields(self, form, index):
  84
+        """A hook for adding extra fields on to each form instance."""
  85
+        pass
  86
+
  87
+    def add_prefix(self, index):
  88
+        return '%s-%s' % (self.prefix, index)
  89
+
  90
+    def _get_clean_data(self):
  91
+        return self.get_clean_data()
  92
+
  93
+    def get_clean_data(self):
  94
+        clean_data_list = []
  95
+        for form in self.get_non_empty_forms():
  96
+            clean_data_list.append(form.clean_data)
  97
+        return clean_data_list
  98
+
  99
+    clean_data = property(_get_clean_data)
  100
+
  101
+    def is_valid(self):
  102
+        for form in self.get_non_empty_forms():
  103
+            if not form.is_valid():
  104
+                return False
  105
+        return True
  106
+
  107
+    def get_non_empty_forms(self):
  108
+        """Return all forms that aren't empty."""
  109
+        return [form for form in self.form_list if not form.is_empty()]
  110
+
  111
+class FormSetWithDeletion(FormSet):
  112
+    """A ``FormSet`` that handles deletion of forms."""
  113
+
  114
+    def add_fields(self, form, index):
  115
+        """Add a delete checkbox to each form."""
  116
+        form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', required=False)
  117
+
  118
+    def get_clean_data(self):
  119
+        self.deleted_data = []
  120
+        clean_data_list = []
  121
+        for form in self.get_non_empty_forms():
  122
+            if form.clean_data[DELETION_FIELD_NAME]:
  123
+                # stick data marked for deletetion in self.deleted_data
  124
+                self.deleted_data.append(form.clean_data)
  125
+            else:
  126
+               clean_data_list.append(form.clean_data)
  127
+        return clean_data_list
  128
+
  129
+class FormSetWithOrdering(FormSet):
  130
+    """A ``FormSet`` that handles re-ordering of forms."""
  131
+
  132
+    def get_non_empty_forms(self):
  133
+        return [form for form in self.form_list if not form.is_empty(exceptions=[ORDERING_FIELD_NAME])]
  134
+
  135
+    def add_fields(self, form, index):
  136
+        """Add an ordering field to each form."""
  137
+        form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', initial=index+1)
  138
+
  139
+    def get_clean_data(self):
  140
+        clean_data_list = []
  141
+        for form in self.get_non_empty_forms():
  142
+            clean_data_list.append(form.clean_data)
  143
+        # sort clean_data by the 'ORDER' field
  144
+        clean_data_list.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
  145
+        return clean_data_list
  146
+
  147
+    def is_valid(self):
  148
+        for form in self.get_non_empty_forms():
  149
+            if not form.is_valid():
  150
+                return False
  151
+        return True
  152
+
  153
+# TODO: handle deletion and ordering in the same FormSet
  154
+# TODO: model integration: form_for_instance and form_for_model type functions
152  tests/regressiontests/forms/tests.py
@@ -2895,6 +2895,158 @@
2895 2895
 >>> p.clean_data
2896 2896
 {'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)}
2897 2897
 
  2898
+# FormSets ####################################################################
  2899
+
  2900
+FormSets allow you to create a bunch of instances of the same form class and
  2901
+get back clean data as a list of dicts.
  2902
+
  2903
+>>> from django.newforms import formsets
  2904
+
  2905
+>>> class ChoiceForm(Form):
  2906
+...     choice = CharField()
  2907
+...     votes = IntegerField()
  2908
+
  2909
+
  2910
+Create an empty form set
  2911
+
  2912
+>>> form_set = formsets.FormSet(ChoiceForm, prefix='choices', auto_id=False)
  2913
+>>> for form in form_set.get_forms():
  2914
+...     print form.as_ul()
  2915
+<li>Choice: <input type="text" name="choices-0-choice" /></li>
  2916
+<li>Votes: <input type="text" name="choices-0-votes" /></li>
  2917
+
  2918
+
  2919
+Forms pre-filled with initial data.
  2920
+
  2921
+>>> initial_data = [
  2922
+...     {'votes': 50, 'choice': u'The Doors', 'id': u'0'},
  2923
+...     {'votes': 51, 'choice': u'The Beatles', 'id': u'1'},
  2924
+... ]
  2925
+
  2926
+>>> form_set = formsets.FormSet(ChoiceForm, initial=initial_data, auto_id=False, prefix='choices')
  2927
+>>> print form_set.management_form.as_ul()
  2928
+<input type="hidden" name="choices-COUNT" value="3" />
  2929
+
  2930
+>>> for form in form_set.get_forms(): # print pre-filled forms
  2931
+...     print form.as_ul()
  2932
+<li>Choice: <input type="text" name="choices-0-choice" value="The Doors" /></li>
  2933
+<li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>
  2934
+<li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" /></li>
  2935
+<li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>
  2936
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
  2937
+<li>Votes: <input type="text" name="choices-2-votes" /></li>
  2938
+
  2939
+
  2940
+Tests for dealing with POSTed data
  2941
+
  2942
+>>> data = {
  2943
+...     'choices-COUNT': u'3', # the number of forms rendered
  2944
+...     'choices-0-choice': u'The Doors',
  2945
+...     'choices-0-votes': u'50',
  2946
+...     'choices-1-choice': u'The Beatles',
  2947
+...     'choices-1-votes': u'51',
  2948
+...     'choices-2-choice': u'',
  2949
+...     'choices-2-votes': u'',
  2950
+... }
  2951
+
  2952
+
  2953
+>>> form_set = formsets.FormSet(ChoiceForm, data, auto_id=False, prefix='choices')
  2954
+>>> print form_set.is_valid()
  2955
+True
  2956
+>>> for data in form_set.clean_data:
  2957
+...     print data
  2958
+{'votes': 50, 'choice': u'The Doors'}
  2959
+{'votes': 51, 'choice': u'The Beatles'}
  2960
+
  2961
+
  2962
+FormSet with deletion fields
  2963
+
  2964
+>>> form_set = formsets.FormSetWithDeletion(ChoiceForm, initial=initial_data, auto_id=False, prefix='choices')
  2965
+>>> for form in form_set.get_forms(): # print pre-filled forms
  2966
+...     print form.as_ul()
  2967
+<li>Choice: <input type="text" name="choices-0-choice" value="The Doors" /></li>
  2968
+<li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>
  2969
+<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
  2970
+<li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" /></li>
  2971
+<li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>
  2972
+<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
  2973
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
  2974
+<li>Votes: <input type="text" name="choices-2-votes" /></li>
  2975
+<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
  2976
+
  2977
+>>> data = {
  2978
+...     'choices-COUNT': u'3', # the number of forms rendered
  2979
+...     'choices-0-choice': u'Fergie',
  2980
+...     'choices-0-votes': u'1000',
  2981
+...     'choices-0-DELETE': u'on', # Delete this choice.
  2982
+...     'choices-1-choice': u'The Decemberists',
  2983
+...     'choices-1-votes': u'150',
  2984
+...     'choices-2-choice': u'Calexico',
  2985
+...     'choices-2-votes': u'90',
  2986
+... }
  2987
+
  2988
+>>> form_set = formsets.FormSetWithDeletion(ChoiceForm, data, auto_id=False, prefix='choices')
  2989
+>>> print form_set.is_valid()
  2990
+True
  2991
+
  2992
+When we access form_set.clean_data, items marked for deletion won't be there,
  2993
+but they *will* be in form_set.deleted_data
  2994
+
  2995
+>>> for data in form_set.clean_data:
  2996
+...     print data
  2997
+{'votes': 150, 'DELETE': False, 'choice': u'The Decemberists'}
  2998
+{'votes': 90, 'DELETE': False, 'choice': u'Calexico'}
  2999
+
  3000
+>>> for data in form_set.deleted_data:
  3001
+...     print data
  3002
+{'votes': 1000, 'DELETE': True, 'choice': u'Fergie'}
  3003
+
  3004
+
  3005
+FormSet with Ordering
  3006
+
  3007
+>>> form_set = formsets.FormSetWithOrdering(ChoiceForm, initial=initial_data, auto_id=False, prefix='choices')
  3008
+>>> for form in form_set.get_forms(): # print pre-filled forms
  3009
+...     print form.as_ul()
  3010
+<li>Choice: <input type="text" name="choices-0-choice" value="The Doors" /></li>
  3011
+<li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>
  3012
+<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
  3013
+<li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" /></li>
  3014
+<li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>
  3015
+<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
  3016
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
  3017
+<li>Votes: <input type="text" name="choices-2-votes" /></li>
  3018
+<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>
  3019
+
  3020
+>>> data = {
  3021
+...     'choices-COUNT': u'4', # the number of forms rendered
  3022
+...     'choices-0-choice': u'Fergie',
  3023
+...     'choices-0-votes': u'1000',
  3024
+...     'choices-0-ORDER': u'3',
  3025
+...     'choices-1-choice': u'The Decemberists',
  3026
+...     'choices-1-votes': u'150',
  3027
+...     'choices-1-ORDER': u'1',
  3028
+...     'choices-2-choice': u'Calexico',
  3029
+...     'choices-2-votes': u'90',
  3030
+...     'choices-2-ORDER': u'2',
  3031
+...     'choices-3-choice': u'',
  3032
+...     'choices-3-votes': u'',
  3033
+...     'choices-3-ORDER': u'4',
  3034
+... }
  3035
+
  3036
+>>> form_set = formsets.FormSetWithOrdering(ChoiceForm, data, auto_id=False, prefix='choices')
  3037
+>>> print form_set.is_valid()
  3038
+True
  3039
+
  3040
+The form_set.clean_data will be in the correct order as specified by the
  3041
+ORDER field from each form.
  3042
+
  3043
+>>> for data in form_set.clean_data:
  3044
+...     print data
  3045
+{'votes': 150, 'ORDER': 1, 'choice': u'The Decemberists'}
  3046
+{'votes': 90, 'ORDER': 2, 'choice': u'Calexico'}
  3047
+{'votes': 1000, 'ORDER': 3, 'choice': u'Fergie'}
  3048
+
  3049
+
2898 3050
 # Forms with NullBooleanFields ################################################
2899 3051
 
2900 3052
 NullBooleanField is a bit of a special case because its presentation (widget)

0 notes on commit e2b49c3

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