Changeset 4953
- Timestamp:
- 04/07/07 00:56:42 (2 years ago)
- Files:
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/branches/newforms-admin/django/newforms/formsets.py
r4836 r4953 5 5 ORDERING_FIELD_NAME = 'ORDER' 6 6 DELETION_FIELD_NAME = 'DELETE' 7 8 def formset_for_form(form, num_extra=1, orderable=False, deletable=False): 9 """Return a FormSet for the given form class.""" 10 attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable} 11 return type(form.__name__ + 'FormSet', (BaseFormSet,), attrs) 7 12 8 13 class ManagementForm(forms.Form): … … 16 21 super(ManagementForm, self).__init__(*args, **kwargs) 17 22 18 class FormSet(object):23 class BaseFormSet(object): 19 24 """A collection of instances of the same Form class.""" 20 25 21 def __init__(self, form_class,data=None, auto_id='id_%s', prefix=None, initial=None):22 self. form_class = form_class26 def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None): 27 self.is_bound = data is not None 23 28 self.prefix = prefix or 'form' 24 29 self.auto_id = auto_id 30 self.data = data 31 self.initial = initial 25 32 # initialization is different depending on whether we recieved data, initial, or nothing 26 33 if data: 27 34 self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix) 28 35 if self.management_form.is_valid(): 29 form_count = self.management_form.clean_data[FORM_COUNT_FIELD_NAME] 36 self.total_forms = self.management_form.clean_data[FORM_COUNT_FIELD_NAME] 37 self.required_forms = self.total_forms - self.num_extra 30 38 else: 31 39 # not sure that ValidationError is the best thing to raise here 32 40 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 41 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)42 self.required_forms = len(initial) 43 self.total_forms = self.required_forms + self.num_extra 44 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 38 45 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) 46 self.required_forms = 0 47 self.total_forms = self.num_extra 48 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 41 49 42 # TODO: initialization needs some cleanup and some restructuring 43 # TODO: allow more than 1 extra blank form to be displayed 50 def _get_form_list(self): 51 """Return a list of Form instances.""" 52 if not hasattr(self, '_form_list'): 53 self._form_list = [] 54 for i in range(0, self.total_forms): 55 kwargs = {'data': self.data, 'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 56 if self.initial and i < self.required_forms: 57 kwargs['initial'] = self.initial[i] 58 form_instance = self.form_class(**kwargs) 59 # HACK: if the form was not completed, replace it with a blank one 60 if self.data and i >= self.required_forms and form_instance.is_empty(): 61 form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i)) 62 self.add_fields(form_instance, i) 63 self._form_list.append(form_instance) 64 return self._form_list 44 65 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 66 form_list = property(_get_form_list) 59 67 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 68 def full_clean(self): 69 """ 70 Cleans all of self.data and populates self.__errors and self.clean_data. 71 """ 72 is_valid = True 73 74 errors = [] 75 if not self.is_bound: # Stop further processing. 76 self.__errors = errors 77 return 78 clean_data = [] 79 deleted_data = [] 80 81 self._form_list = [] 82 # step backwards through the forms so when we hit the first filled one 83 # we can easily require the rest without backtracking 84 required = False 85 for i in range(self.total_forms-1, -1, -1): 86 kwargs = {'data': self.data, 'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 87 88 # prep initial data if there is some 89 if self.initial and i < self.required_forms: 90 kwargs['initial'] = self.initial[i] 91 92 # create the form instance 93 form = self.form_class(**kwargs) 94 self.add_fields(form, i) 95 96 if self.data and (i < self.required_forms or not form.is_empty(exceptions=[ORDERING_FIELD_NAME])): 97 required = True # forms cannot be empty anymore 98 99 # HACK: if the form is empty and not required, replace it with a blank one 100 # this is necessary to keep form.errors empty 101 if not required and self.data and form.is_empty(exceptions=[ORDERING_FIELD_NAME]): 102 form = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i)) 103 self.add_fields(form, i) 104 else: 105 # if the formset is still vaild overall and this form instance 106 # is valid, keep appending to clean_data 107 if is_valid and form.is_valid(): 108 if self.deletable and form.clean_data[DELETION_FIELD_NAME]: 109 deleted_data.append(form.clean_data) 110 else: 111 clean_data.append(form.clean_data) 112 else: 113 is_valid = False 114 # append to errors regardless 115 errors.append(form.errors) 116 self._form_list.append(form) 72 117 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 118 deleted_data.reverse() 119 if self.orderable: 120 clean_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) 121 else: 122 clean_data.reverse() 123 errors.reverse() 124 self._form_list.reverse() 125 126 if is_valid: 127 self.clean_data = clean_data 128 self.deleted_data = deleted_data 129 self.errors = errors 130 self._is_valid = is_valid 79 131 80 def get_forms(self): 81 return self.form_list 82 132 # TODO: user defined formset validation. 133 83 134 def add_fields(self, form, index): 84 135 """A hook for adding extra fields on to each form instance.""" 85 pass 136 if self.orderable: 137 form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', initial=index+1) 138 if self.deletable: 139 form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', required=False) 86 140 87 141 def add_prefix(self, index): 88 142 return '%s-%s' % (self.prefix, index) 89 143 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_list98 99 clean_data = property(_get_clean_data)100 101 144 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 145 self.full_clean() 146 return self._is_valid 152 147 153 148 # TODO: handle deletion and ordering in the same FormSet django/branches/newforms-admin/tests/regressiontests/forms/tests.py
r4940 r4953 2 2 from localflavor import localflavor_tests 3 3 from regressions import regression_tests 4 from formsets import formset_tests 4 5 5 6 form_tests = r""" … … 2899 2900 {'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)} 2900 2901 2901 # FormSets ####################################################################2902 2903 FormSets allow you to create a bunch of instances of the same form class and2904 get back clean data as a list of dicts.2905 2906 >>> from django.newforms import formsets2907 2908 >>> class ChoiceForm(Form):2909 ... choice = CharField()2910 ... votes = IntegerField()2911 2912 2913 Create an empty form set2914 2915 >>> form_set = formsets.FormSet(ChoiceForm, prefix='choices', auto_id=False)2916 >>> for form in form_set.get_forms():2917 ... print form.as_ul()2918 <li>Choice: <input type="text" name="choices-0-choice" /></li>2919 <li>Votes: <input type="text" name="choices-0-votes" /></li>2920 2921 2922 Forms pre-filled with initial data.2923 2924 >>> initial_data = [2925 ... {'votes': 50, 'choice': u'The Doors', 'id': u'0'},2926 ... {'votes': 51, 'choice': u'The Beatles', 'id': u'1'},2927 ... ]2928 2929 >>> form_set = formsets.FormSet(ChoiceForm, initial=initial_data, auto_id=False, prefix='choices')2930 >>> print form_set.management_form.as_ul()2931 <input type="hidden" name="choices-COUNT" value="3" />2932 2933 >>> for form in form_set.get_forms(): # print pre-filled forms2934 ... print form.as_ul()2935 <li>Choice: <input type="text" name="choices-0-choice" value="The Doors" /></li>2936 <li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>2937 <li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" /></li>2938 <li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>2939 <li>Choice: <input type="text" name="choices-2-choice" /></li>2940 <li>Votes: <input type="text" name="choices-2-votes" /></li>2941 2942 2943 Tests for dealing with POSTed data2944 2945 >>> data = {2946 ... 'choices-COUNT': u'3', # the number of forms rendered2947 ... 'choices-0-choice': u'The Doors',2948 ... 'choices-0-votes': u'50',2949 ... 'choices-1-choice': u'The Beatles',2950 ... 'choices-1-votes': u'51',2951 ... 'choices-2-choice': u'',2952 ... 'choices-2-votes': u'',2953 ... }2954 2955 2956 >>> form_set = formsets.FormSet(ChoiceForm, data, auto_id=False, prefix='choices')2957 >>> print form_set.is_valid()2958 True2959 >>> for data in form_set.clean_data:2960 ... print data2961 {'votes': 50, 'choice': u'The Doors'}2962 {'votes': 51, 'choice': u'The Beatles'}2963 2964 2965 FormSet with deletion fields2966 2967 >>> form_set = formsets.FormSetWithDeletion(ChoiceForm, initial=initial_data, auto_id=False, prefix='choices')2968 >>> for form in form_set.get_forms(): # print pre-filled forms2969 ... print form.as_ul()2970 <li>Choice: <input type="text" name="choices-0-choice" value="The Doors" /></li>2971 <li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>2972 <li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>2973 <li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" /></li>2974 <li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>2975 <li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>2976 <li>Choice: <input type="text" name="choices-2-choice" /></li>2977 <li>Votes: <input type="text" name="choices-2-votes" /></li>2978 <li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>2979 2980 >>> data = {2981 ... 'choices-COUNT': u'3', # the number of forms rendered2982 ... 'choices-0-choice': u'Fergie',2983 ... 'choices-0-votes': u'1000',2984 ... 'choices-0-DELETE': u'on', # Delete this choice.2985 ... 'choices-1-choice': u'The Decemberists',2986 ... 'choices-1-votes': u'150',2987 ... 'choices-2-choice': u'Calexico',2988 ... 'choices-2-votes': u'90',2989 ... }2990 2991 >>> form_set = formsets.FormSetWithDeletion(ChoiceForm, data, auto_id=False, prefix='choices')2992 >>> print form_set.is_valid()2993 True2994 2995 When we access form_set.clean_data, items marked for deletion won't be there,2996 but they *will* be in form_set.deleted_data2997 2998 >>> for data in form_set.clean_data:2999 ... print data3000 {'votes': 150, 'DELETE': False, 'choice': u'The Decemberists'}3001 {'votes': 90, 'DELETE': False, 'choice': u'Calexico'}3002 3003 >>> for data in form_set.deleted_data:3004 ... print data3005 {'votes': 1000, 'DELETE': True, 'choice': u'Fergie'}3006 3007 3008 FormSet with Ordering3009 3010 >>> form_set = formsets.FormSetWithOrdering(ChoiceForm, initial=initial_data, auto_id=False, prefix='choices')3011 >>> for form in form_set.get_forms(): # print pre-filled forms3012 ... print form.as_ul()3013 <li>Choice: <input type="text" name="choices-0-choice" value="The Doors" /></li>3014 <li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>3015 <li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>3016 <li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" /></li>3017 <li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>3018 <li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>3019 <li>Choice: <input type="text" name="choices-2-choice" /></li>3020 <li>Votes: <input type="text" name="choices-2-votes" /></li>3021 <li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>3022 3023 >>> data = {3024 ... 'choices-COUNT': u'4', # the number of forms rendered3025 ... 'choices-0-choice': u'Fergie',3026 ... 'choices-0-votes': u'1000',3027 ... 'choices-0-ORDER': u'3',3028 ... 'choices-1-choice': u'The Decemberists',3029 ... 'choices-1-votes': u'150',3030 ... 'choices-1-ORDER': u'1',3031 ... 'choices-2-choice': u'Calexico',3032 ... 'choices-2-votes': u'90',3033 ... 'choices-2-ORDER': u'2',3034 ... 'choices-3-choice': u'',3035 ... 'choices-3-votes': u'',3036 ... 'choices-3-ORDER': u'4',3037 ... }3038 3039 >>> form_set = formsets.FormSetWithOrdering(ChoiceForm, data, auto_id=False, prefix='choices')3040 >>> print form_set.is_valid()3041 True3042 3043 The form_set.clean_data will be in the correct order as specified by the3044 ORDER field from each form.3045 3046 >>> for data in form_set.clean_data:3047 ... print data3048 {'votes': 150, 'ORDER': 1, 'choice': u'The Decemberists'}3049 {'votes': 90, 'ORDER': 2, 'choice': u'Calexico'}3050 {'votes': 1000, 'ORDER': 3, 'choice': u'Fergie'}3051 3052 3053 2902 # Forms with NullBooleanFields ################################################ 3054 2903 … … 3452 3301 'localflavor': localflavor_tests, 3453 3302 'regressions': regression_tests, 3303 'formset_tests': formset_tests, 3454 3304 } 3455 3305
