Django

Code

Changeset 4953

Show
Ignore:
Timestamp:
04/07/07 00:56:42 (2 years ago)
Author:
jkocherhans
Message:

newforms-admin: Rewrote most of FormSets? and associated tests. They now handle an arbitrary number of extra forms, and deletion and ordering at the same time. BaseFormSet? still needs some cleanup, but the tests pass.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/branches/newforms-admin/django/newforms/formsets.py

    r4836 r4953  
    55ORDERING_FIELD_NAME = 'ORDER' 
    66DELETION_FIELD_NAME = 'DELETE' 
     7 
     8def 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) 
    712 
    813class ManagementForm(forms.Form): 
     
    1621        super(ManagementForm, self).__init__(*args, **kwargs) 
    1722 
    18 class FormSet(object): 
     23class BaseFormSet(object): 
    1924    """A collection of instances of the same Form class.""" 
    2025 
    21     def __init__(self, form_class, data=None, auto_id='id_%s', prefix=None, initial=None): 
    22         self.form_class = form_class 
     26    def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None): 
     27        self.is_bound = data is not None 
    2328        self.prefix = prefix or 'form' 
    2429        self.auto_id = auto_id 
     30        self.data = data 
     31        self.initial = initial 
    2532        # initialization is different depending on whether we recieved data, initial, or nothing 
    2633        if data: 
    2734            self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix) 
    2835            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 
    3038            else: 
    3139                # not sure that ValidationError is the best thing to raise here 
    3240                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) 
    3441        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
    3845        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) 
    4149 
    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 
    4465 
    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) 
    5967 
    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) 
    72117 
    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 
    79131 
    80     def get_forms(self): 
    81         return self.form_list 
    82  
     132        # TODO: user defined formset validation. 
     133         
    83134    def add_fields(self, form, index): 
    84135        """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) 
    86140 
    87141    def add_prefix(self, index): 
    88142        return '%s-%s' % (self.prefix, index) 
    89143 
    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  
    101144    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 
    152147 
    153148# TODO: handle deletion and ordering in the same FormSet 
  • django/branches/newforms-admin/tests/regressiontests/forms/tests.py

    r4940 r4953  
    22from localflavor import localflavor_tests 
    33from regressions import regression_tests 
     4from formsets import formset_tests 
    45 
    56form_tests = r""" 
     
    28992900{'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)} 
    29002901 
    2901 # FormSets #################################################################### 
    2902  
    2903 FormSets allow you to create a bunch of instances of the same form class and 
    2904 get back clean data as a list of dicts. 
    2905  
    2906 >>> from django.newforms import formsets 
    2907  
    2908 >>> class ChoiceForm(Form): 
    2909 ...     choice = CharField() 
    2910 ...     votes = IntegerField() 
    2911  
    2912  
    2913 Create an empty form set 
    2914  
    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 forms 
    2934 ...     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 data 
    2944  
    2945 >>> data = { 
    2946 ...     'choices-COUNT': u'3', # the number of forms rendered 
    2947 ...     '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 True 
    2959 >>> for data in form_set.clean_data: 
    2960 ...     print data 
    2961 {'votes': 50, 'choice': u'The Doors'} 
    2962 {'votes': 51, 'choice': u'The Beatles'} 
    2963  
    2964  
    2965 FormSet with deletion fields 
    2966  
    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 forms 
    2969 ...     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 rendered 
    2982 ...     '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 True 
    2994  
    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_data 
    2997  
    2998 >>> for data in form_set.clean_data: 
    2999 ...     print data 
    3000 {'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 data 
    3005 {'votes': 1000, 'DELETE': True, 'choice': u'Fergie'} 
    3006  
    3007  
    3008 FormSet with Ordering 
    3009  
    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 forms 
    3012 ...     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 rendered 
    3025 ...     '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 True 
    3042  
    3043 The form_set.clean_data will be in the correct order as specified by the 
    3044 ORDER field from each form. 
    3045  
    3046 >>> for data in form_set.clean_data: 
    3047 ...     print data 
    3048 {'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  
    30532902# Forms with NullBooleanFields ################################################ 
    30542903 
     
    34523301    'localflavor': localflavor_tests, 
    34533302    'regressions': regression_tests, 
     3303    'formset_tests': formset_tests, 
    34543304} 
    34553305