Changeset 7270
- Timestamp:
- 03/17/08 12:55:16 (1 year ago)
- Files:
-
- django/branches/newforms-admin/django/contrib/admin/options.py (modified) (11 diffs)
- django/branches/newforms-admin/django/newforms/formsets.py (modified) (7 diffs)
- django/branches/newforms-admin/django/newforms/forms.py (modified) (5 diffs)
- django/branches/newforms-admin/django/newforms/models.py (modified) (10 diffs)
- django/branches/newforms-admin/django/newforms/widgets.py (modified) (4 diffs)
- django/branches/newforms-admin/tests/modeltests/model_formsets/models.py (modified) (15 diffs)
- django/branches/newforms-admin/tests/regressiontests/forms/formsets.py (modified) (34 diffs)
- django/branches/newforms-admin/tests/regressiontests/forms/forms.py (modified) (1 diff)
- django/branches/newforms-admin/tests/regressiontests/forms/tests.py (modified) (1 diff)
- django/branches/newforms-admin/tests/regressiontests/forms/widgets.py (modified) (3 diffs)
- django/branches/newforms-admin/tests/regressiontests/inline_formsets/models.py (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/branches/newforms-admin/django/contrib/admin/options.py
r7195 r7270 2 2 from django import newforms as forms 3 3 from django.newforms.formsets import all_valid 4 from django.newforms.models import _modelform_factory, _inlineformset_factory 4 5 from django.contrib.contenttypes.models import ContentType 5 6 from django.contrib.admin import widgets … … 341 342 else: 342 343 fields = None 343 return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)344 return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 344 345 345 346 def form_change(self, request, obj): … … 351 352 else: 352 353 fields = None 353 return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)354 return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 354 355 355 356 def save_add(self, request, model, form, formsets, post_url_continue): … … 497 498 form = ModelForm(request.POST, request.FILES) 498 499 for FormSet in self.formsets_add(request): 499 inline_formset = FormSet( obj, data=request.POST, files=request.FILES)500 inline_formset = FormSet(data=request.POST, files=request.FILES, instance=obj) 500 501 inline_formsets.append(inline_formset) 501 502 if all_valid(inline_formsets) and form.is_valid(): … … 504 505 form = ModelForm(initial=request.GET) 505 506 for FormSet in self.formsets_add(request): 506 inline_formset = FormSet( obj)507 inline_formset = FormSet(instance=obj) 507 508 inline_formsets.append(inline_formset) 508 509 … … 554 555 inline_formsets = [] 555 556 if request.method == 'POST': 556 form = ModelForm(request.POST, request.FILES )557 form = ModelForm(request.POST, request.FILES, instance=obj) 557 558 for FormSet in self.formsets_change(request, obj): 558 inline_formset = FormSet( obj, request.POST, request.FILES)559 inline_formset = FormSet(request.POST, request.FILES, instance=obj) 559 560 inline_formsets.append(inline_formset) 560 561 … … 562 563 return self.save_change(request, model, form, inline_formsets) 563 564 else: 564 form = ModelForm( )565 form = ModelForm(instance=obj) 565 566 for FormSet in self.formsets_change(request, obj): 566 inline_formset = FormSet( obj)567 inline_formset = FormSet(instance=obj) 567 568 inline_formsets.append(inline_formset) 568 569 … … 741 742 else: 742 743 fields = None 743 return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)744 return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 744 745 745 746 def formset_change(self, request, obj): … … 749 750 else: 750 751 fields = None 751 return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)752 return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 752 753 753 754 def fieldsets_add(self, request): 754 755 if self.declared_fieldsets: 755 756 return self.declared_fieldsets 756 form = self.formset_add(request).form _class757 form = self.formset_add(request).form 757 758 return [(None, {'fields': form.base_fields.keys()})] 758 759 … … 760 761 if self.declared_fieldsets: 761 762 return self.declared_fieldsets 762 form = self.formset_change(request, obj).form _class763 form = self.formset_change(request, obj).form 763 764 return [(None, {'fields': form.base_fields.keys()})] 764 765 … … 779 780 780 781 def __iter__(self): 781 for form, original in zip(self.formset. change_forms, self.formset.get_queryset()):782 for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): 782 783 yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original) 783 for form in self.formset. add_forms:784 for form in self.formset.extra_forms: 784 785 yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None) 785 786 786 787 def fields(self): 787 788 for field_name in flatten_fieldsets(self.fieldsets): 788 yield self.formset.form _class.base_fields[field_name]789 yield self.formset.form.base_fields[field_name] 789 790 790 791 class InlineAdminForm(AdminForm): django/branches/newforms-admin/django/newforms/formsets.py
r6419 r7270 1 1 from forms import Form 2 from django.utils.encoding import StrAndUnicode 2 3 from fields import IntegerField, BooleanField 3 from widgets import HiddenInput, Media4 from widgets import HiddenInput, TextInput 4 5 from util import ErrorList, ValidationError 5 6 6 __all__ = ('BaseFormSet', ' formset_for_form', 'all_valid')7 __all__ = ('BaseFormSet', 'all_valid') 7 8 8 9 # special field names 9 FORM_COUNT_FIELD_NAME = 'COUNT' 10 TOTAL_FORM_COUNT = 'TOTAL_FORMS' 11 INITIAL_FORM_COUNT = 'INITIAL_FORMS' 10 12 ORDERING_FIELD_NAME = 'ORDER' 11 13 DELETION_FIELD_NAME = 'DELETE' … … 18 20 """ 19 21 def __init__(self, *args, **kwargs): 20 self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 22 self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 23 self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 21 24 super(ManagementForm, self).__init__(*args, **kwargs) 22 25 23 class BaseFormSet(object): 24 """A collection of instances of the same Form class.""" 25 26 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 26 class BaseFormSet(StrAndUnicode): 27 """ 28 A collection of instances of the same Form class. 29 """ 30 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 27 31 initial=None, error_class=ErrorList): 28 32 self.is_bound = data is not None or files is not None … … 33 37 self.initial = initial 34 38 self.error_class = error_class 39 self._errors = None 40 self._non_form_errors = None 35 41 # initialization is different depending on whether we recieved data, initial, or nothing 36 42 if data or files: 37 43 self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix) 38 44 if self.management_form.is_valid(): 39 self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME] 40 self.required_forms = self.total_forms - self.num_extra 41 self.change_form_count = self.total_forms - self.num_extra 45 self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT] 46 self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT] 42 47 else: 43 # not sure that ValidationError is the best thing to raise here44 48 raise ValidationError('ManagementForm data is missing or has been tampered with') 45 49 elif initial: 46 self.change_form_count = len(initial) 47 self.required_forms = len(initial) 48 self.total_forms = self.required_forms + self.num_extra 49 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 50 self._initial_form_count = len(initial) 51 self._total_form_count = self._initial_form_count + self.extra 50 52 else: 51 self.change_form_count = 0 52 self.required_forms = 0 53 self.total_forms = self.num_extra 54 self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 55 56 def _get_add_forms(self): 57 """Return a list of all the add forms in this ``FormSet``.""" 58 FormClass = self.form_class 59 if not hasattr(self, '_add_forms'): 60 add_forms = [] 61 for i in range(self.change_form_count, self.total_forms): 62 kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 63 if self.data: 64 kwargs['data'] = self.data 65 if self.files: 66 kwargs['files'] = self.files 67 add_form = FormClass(**kwargs) 68 self.add_fields(add_form, i) 69 add_forms.append(add_form) 70 self._add_forms = add_forms 71 return self._add_forms 72 add_forms = property(_get_add_forms) 73 74 def _get_change_forms(self): 75 """Return a list of all the change forms in this ``FormSet``.""" 76 FormClass = self.form_class 77 if not hasattr(self, '_change_forms'): 78 change_forms = [] 79 for i in range(0, self.change_form_count): 80 kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 81 if self.data: 82 kwargs['data'] = self.data 83 if self.files: 84 kwargs['files'] = self.files 85 if self.initial: 86 kwargs['initial'] = self.initial[i] 87 change_form = FormClass(**kwargs) 88 self.add_fields(change_form, i) 89 change_forms.append(change_form) 90 self._change_forms= change_forms 91 return self._change_forms 92 change_forms = property(_get_change_forms) 93 94 def _forms(self): 95 return self.change_forms + self.add_forms 96 forms = property(_forms) 53 self._initial_form_count = 0 54 self._total_form_count = self.extra 55 initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count} 56 self.management_form = ManagementForm(initial=initial, auto_id=auto_id, prefix=prefix) 57 58 # instantiate all the forms and put them in self.forms 59 self.forms = [] 60 for i in range(self._total_form_count): 61 self.forms.append(self._construct_form(i)) 62 63 def __unicode__(self): 64 return self.as_table() 65 66 def _construct_form(self, i): 67 """ 68 Instantiates and returns the i-th form instance in a formset. 69 """ 70 kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 71 if self.data or self.files: 72 kwargs['data'] = self.data 73 kwargs['files'] = self.files 74 if self.initial: 75 try: 76 kwargs['initial'] = self.initial[i] 77 except IndexError: 78 pass 79 # Allow extra forms to be empty. 80 if i >= self._initial_form_count: 81 kwargs['empty_permitted'] = True 82 form = self.form(**kwargs) 83 self.add_fields(form, i) 84 return form 85 86 def _get_initial_forms(self): 87 """Return a list of all the intial forms in this formset.""" 88 return self.forms[:self._initial_form_count] 89 initial_forms = property(_get_initial_forms) 90 91 def _get_extra_forms(self): 92 """Return a list of all the extra forms in this formset.""" 93 return self.forms[self._initial_form_count:] 94 extra_forms = property(_get_extra_forms) 95 96 # Maybe this should just go away? 97 def _get_cleaned_data(self): 98 """ 99 Returns a list of form.cleaned_data dicts for every form in self.forms. 100 """ 101 if not self.is_valid(): 102 raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__) 103 return [form.cleaned_data for form in self.forms] 104 cleaned_data = property(_get_cleaned_data) 105 106 def _get_deleted_forms(self): 107 """ 108 Returns a list of forms that have been marked for deletion. Raises an 109 AttributeError is deletion is not allowed. 110 """ 111 if not self.is_valid() or not self.can_delete: 112 raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__) 113 # construct _deleted_form_indexes which is just a list of form indexes 114 # that have had their deletion widget set to True 115 if not hasattr(self, '_deleted_form_indexes'): 116 self._deleted_form_indexes = [] 117 for i in range(0, self._total_form_count): 118 form = self.forms[i] 119 # if this is an extra form and hasn't changed, don't consider it 120 if i >= self._initial_form_count and not form.has_changed(): 121 continue 122 if form.cleaned_data[DELETION_FIELD_NAME]: 123 self._deleted_form_indexes.append(i) 124 return [self.forms[i] for i in self._deleted_form_indexes] 125 deleted_forms = property(_get_deleted_forms) 126 127 def _get_ordered_forms(self): 128 """ 129 Returns a list of form in the order specified by the incoming data. 130 Raises an AttributeError is deletion is not allowed. 131 """ 132 if not self.is_valid() or not self.can_order: 133 raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__) 134 # Construct _ordering, which is a list of (form_index, order_field_value) 135 # tuples. After constructing this list, we'll sort it by order_field_value 136 # so we have a way to get to the form indexes in the order specified 137 # by the form data. 138 if not hasattr(self, '_ordering'): 139 self._ordering = [] 140 for i in range(0, self._total_form_count): 141 form = self.forms[i] 142 # if this is an extra form and hasn't changed, don't consider it 143 if i >= self._initial_form_count and not form.has_changed(): 144 continue 145 # don't add data marked for deletion to self.ordered_data 146 if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: 147 continue 148 # A sort function to order things numerically ascending, but 149 # None should be sorted below anything else. Allowing None as 150 # a comparison value makes it so we can leave ordering fields 151 # blamk. 152 def compare_ordering_values(x, y): 153 if x[1] is None: 154 return 1 155 if y[1] is None: 156 return -1 157 return x[1] - y[1] 158 self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME])) 159 # After we're done populating self._ordering, sort it. 160 self._ordering.sort(compare_ordering_values) 161 # Return a list of form.cleaned_data dicts in the order spcified by 162 # the form data. 163 return [self.forms[i[0]] for i in self._ordering] 164 ordered_forms = property(_get_ordered_forms) 97 165 98 166 def non_form_errors(self): … … 102 170 are none. 103 171 """ 104 if hasattr(self, '_non_form_errors'):172 if self._non_form_errors is not None: 105 173 return self._non_form_errors 106 174 return self.error_class() 107 175 176 def _get_errors(self): 177 """ 178 Returns a list of form.errors for every form in self.forms. 179 """ 180 if self._errors is None: 181 self.full_clean() 182 return self._errors 183 errors = property(_get_errors) 184 185 def is_valid(self): 186 """ 187 Returns True if form.errors is empty for every form in self.forms. 188 """ 189 if not self.is_bound: 190 return False 191 # We loop over every form.errors here rather than short circuiting on the 192 # first failure to make sure validation gets triggered for every form. 193 forms_valid = True 194 for errors in self.errors: 195 if bool(errors): 196 forms_valid = False 197 return forms_valid and not bool(self.non_form_errors()) 198 108 199 def full_clean(self): 109 """Cleans all of self.data and populates self.__errors and self.cleaned_data.""" 110 self._is_valid = True # Assume the formset is valid until proven otherwise. 111 errors = [] 200 """ 201 Cleans all of self.data and populates self._errors. 202 """ 203 self._errors = [] 112 204 if not self.is_bound: # Stop further processing. 113 self.__errors = errors114 205 return 115 self.cleaned_data = [] 116 self.deleted_data = [] 117 # Process change forms 118 for form in self.change_forms: 119 if form.is_valid(): 120 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 121 self.deleted_data.append(form.cleaned_data) 122 else: 123 self.cleaned_data.append(form.cleaned_data) 124 else: 125 self._is_valid = False 126 errors.append(form.errors) 127 # Process add forms in reverse so we can easily tell when the remaining 128 # ones should be required. 129 reamining_forms_required = False 130 add_errors = [] 131 for i in range(len(self.add_forms)-1, -1, -1): 132 form = self.add_forms[i] 133 # If an add form is empty, reset it so it won't have any errors 134 if form.is_empty([ORDERING_FIELD_NAME]) and not reamining_forms_required: 135 form.reset() 136 continue 137 else: 138 reamining_forms_required = True 139 if form.is_valid(): 140 self.cleaned_data.append(form.cleaned_data) 141 else: 142 self._is_valid = False 143 add_errors.append(form.errors) 144 add_errors.reverse() 145 errors.extend(add_errors) 146 # Sort cleaned_data if the formset is orderable. 147 if self.orderable: 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 206 for i in range(0, self._total_form_count): 207 form = self.forms[i] 208 self._errors.append(form.errors) 209 # Give self.clean() a chance to do cross-form validation. 150 210 try: 151 self.clean ed_data = self.clean()211 self.clean() 152 212 except ValidationError, e: 153 213 self._non_form_errors = e.messages 154 self._is_valid = False155 self.errors = errors156 # If there were errors, be consistent with forms and remove the157 # cleaned_data and deleted_data attributes.158 if not self._is_valid:159 delattr(self, 'cleaned_data')160 delattr(self, 'deleted_data')161 214 162 215 def clean(self): … … 167 220 via formset.non_form_errors() 168 221 """ 169 return self.cleaned_data222 pass 170 223 171 224 def add_fields(self, form, index): 172 225 """A hook for adding extra fields on to each form instance.""" 173 if self.orderable: 174 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1) 175 if self.deletable: 226 if self.can_order: 227 # Only pre-fill the ordering field for initial forms. 228 if index < self._initial_form_count: 229 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False) 230 else: 231 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False) 232 if self.can_delete: 176 233 form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False) 177 234 … … 179 236 return '%s-%s' % (self.prefix, index) 180 237 181 def is_valid(self): 182 if not self.is_bound: 183 return False 184 self.full_clean() 185 return self._is_valid 238 def is_multipart(self): 239 """ 240 Returns True if the formset needs to be multipart-encrypted, i.e. it 241 has FileInput. Otherwise, False. 242 """ 243 return self.forms[0].is_multipart() 186 244 187 245 def _get_media(self): 188 # All the forms on a FormSet are the same, so you only need to 246 # All the forms on a FormSet are the same, so you only need to 189 247 # interrogate the first form for media. 190 248 if self.forms: … … 193 251 return Media() 194 252 media = property(_get_media) 195 196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False): 253 254 def as_table(self): 255 "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>." 256 # XXX: there is no semantic division between forms here, there 257 # probably should be. It might make sense to render each form as a 258 # table row with each field as a td. 259 forms = u' '.join([form.as_table() for form in self.forms]) 260 return u'\n'.join([unicode(self.management_form), forms]) 261 262 # XXX: This API *will* change. Use at your own risk. 263 def _formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False): 197 264 """Return a FormSet for the given form class.""" 198 attrs = {'form _class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}265 attrs = {'form': form, 'extra': extra, 'can_order': can_order, 'can_delete': can_delete} 199 266 return type(form.__name__ + 'FormSet', (formset,), attrs) 200 267 django/branches/newforms-admin/django/newforms/forms.py
r7233 r7270 70 70 # class, not to the Form class. 71 71 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 72 initial=None, error_class=ErrorList, label_suffix=':'): 72 initial=None, error_class=ErrorList, label_suffix=':', 73 empty_permitted=False): 73 74 self.is_bound = data is not None or files is not None 74 75 self.data = data or {} … … 79 80 self.error_class = error_class 80 81 self.label_suffix = label_suffix 82 self.empty_permitted = empty_permitted 81 83 self._errors = None # Stores the errors after clean() has been called. 82 84 … … 190 192 return self.errors.get(NON_FIELD_ERRORS, self.error_class()) 191 193 192 def is_empty(self, exceptions=None):193 """194 Returns True if this form has been bound and all fields that aren't195 listed in exceptions are empty.196 """197 # TODO: This could probably use some optimization198 exceptions = exceptions or []199 for name, field in self.fields.items():200 if name in exceptions:201 continue202 # value_from_datadict() gets the data from the data dictionaries.203 # Each widget type knows how to retrieve its own data, because some204 # widgets split data over several HTML fields.205 value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))206 if not field.widget.is_empty(value):207 return False208 return True209 210 194 def full_clean(self): 211 195 """ … … 217 201 return 218 202 self.cleaned_data = {} 203 # If the form is permitted to be empty, and none of the form data has 204 # changed from the initial data, short circuit any validation. 205 if self.empty_permitted and not self.has_changed(): 206 return 219 207 for name, field in self.fields.items(): 220 208 # value_from_datadict() gets the data from the data dictionaries. … … 252 240 return self.cleaned_data 253 241 254 def reset(self): 255 """Return this form to the state it was in before data was passed to it.""" 256 self.data = {} 257 self.is_bound = False 258 self.__errors = None 242 def has_changed(self): 243 """ 244 Returns True if data differs from initial. 245 """ 246 # XXX: For now we're asking the individual widgets whether or not the 247 # data has changed. It would probably be more efficient to hash the 248 # initial data, store it in a hidden field, and compare a hash of the 249 # submitted data, but we'd need a way to easily get the string value 250 # for a given field. Right now, that logic is embedded in the render 251 # method of each widget. 252 for name, field in self.fields.items(): 253 prefixed_name = self.add_prefix(name) 254 data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) 255 initial_value = self.initial.get(name, field.initial) 256 if field.widget._has_changed(initial_value, data_value): 257 #print field 258 return True 259 return False 259 260 260 261 def _get_media(self): django/branches/newforms-admin/django/newforms/models.py
r7121 r7270 14 14 from forms import BaseForm, get_declared_fields 15 15 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 16 from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME17 16 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 17 from formsets import BaseFormSet, _formset_factory, DELETION_FIELD_NAME 18 18 19 19 __all__ = ( 20 20 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 21 21 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 22 'formset_for_model', 'inline_formset',23 22 'ModelChoiceField', 'ModelMultipleChoiceField', 24 23 ) … … 246 245 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 247 246 initial=None, error_class=ErrorList, label_suffix=':', 248 instance=None):247 empty_permitted=False, instance=None): 249 248 opts = self._meta 250 249 if instance is None: … … 258 257 if initial is not None: 259 258 object_data.update(initial) 260 BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix) 259 BaseForm.__init__(self, data, files, auto_id, prefix, object_data, 260 error_class, label_suffix, empty_permitted) 261 261 262 262 def save(self, commit=True): … … 277 277 __metaclass__ = ModelFormMetaclass 278 278 279 280 # Fields ##################################################################### 281 282 class QuerySetIterator(object): 283 def __init__(self, queryset, empty_label, cache_choices): 279 # XXX: This API *will* change. Use at your own risk. 280 def _modelform_factory(model, form=BaseForm, fields=None, exclude=None, 281 formfield_callback=lambda f: f.formfield()): 282 # HACK: we should be able to construct a ModelForm without creating 283 # and passing in a temporary inner class 284 class Meta: 285 pass 286 setattr(Meta, 'model', model) 287 setattr(Meta, 'fields', fields) 288 setattr(Meta, 'exclude', exclude) 289 class_name = model.__name__ + 'Form' 290 return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta}, 291 formfield_callback=formfield_callback) 292 293 294 # ModelFormSets ############################################################## 295 296 class BaseModelFormSet(BaseFormSet): 297 """ 298 A ``FormSet`` for editing a queryset and/or adding new objects to it. 299 """ 300 model = None 301 302 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None): 284 303 self.queryset = queryset 285 self.empty_label = empty_label286 self.cache_choices = cache_choices287 288 def __iter__(self):289 if self.empty_label is not None:290 yield (u"", self.empty_label)291 for obj in self.queryset:292 yield (obj.pk, smart_unicode(obj))293 # Clear the QuerySet cache if required.294 if not self.cache_choices:295 self.queryset._result_cache = None296 297 class ModelChoiceField(ChoiceField):298 """A ChoiceField whose choices are a model QuerySet."""299 # This class is a subclass of ChoiceField for purity, but it doesn't300 # actually use any of ChoiceField's implementation.301 default_error_messages = {302 'invalid_choice': _(u'Select a valid choice. That choice is not one of'303 u' the available choices.'),304 }305 306 def __init__(self, queryset, empty_label=u"---------", cache_choices=False,307 required=True, widget=Select, label=None, initial=None,308 help_text=None, *args, **kwargs):309 self.empty_label = empty_label310 self.cache_choices = cache_choices311 # Call Field instead of ChoiceField __init__() because we don't need312 # ChoiceField.__init__().313 Field.__init__(self, required, widget, label, initial, help_text,314 *args, **kwargs)315 self.queryset = queryset316 317 def _get_queryset(self):318 return self._queryset319 320 def _set_queryset(self, queryset):321 self._queryset = queryset322 self.widget.choices = self.choices323 324 queryset = property(_get_queryset, _set_queryset)325 326 def _get_choices(self):327 # If self._choices is set, then somebody must have manually set328 # the property self.choices. In this case, just return self._choices.329 if hasattr(self, '_choices'):330 return self._choices331 # Otherwise, execute the QuerySet in self.queryset to determine the332 # choices dynamically. Return a fresh QuerySetIterator that has not333 # been consumed. Note that we're instantiating a new QuerySetIterator334 # *each* time _get_choices() is called (and, thus, each time335 # self.choices is accessed) so that we can ensure the QuerySet has not336 # been consumed.337 return QuerySetIterator(self.queryset, self.empty_label,338 self.cache_choices)339 340 def _set_choices(self, value):341 # This method is copied from ChoiceField._set_choices(). It's necessary342 # because property() doesn't allow a subclass to overwrite only343 # _get_choices without implementing _set_choices.344 self._choices = self.widget.choices = list(value)345 346 choices = property(_get_choices, _set_choices)347 348 def clean(self, value):349 Field.clean(self, value)350 if value in EMPTY_VALUES:351 return None352 try:353 value = self.queryset.get(pk=value)354 except self.queryset.model.DoesNotExist:355 raise ValidationError(self.error_messages['invalid_choice'])356 return value357 358 class ModelMultipleChoiceField(ModelChoiceField):359 """A MultipleChoiceField whose choices are a model QuerySet."""360 hidden_widget = MultipleHiddenInput361 default_error_messages = {362 'list': _(u'Enter a list of values.'),363 'invalid_choice': _(u'Select a valid choice. %s is not one of the'364 u' available choices.'),365 }366 367 def __init__(self, queryset, cache_choices=False, required=True,368 widget=SelectMultiple, label=None, initial=None,369 help_text=None, *args, **kwargs):370 super(ModelMultipleChoiceField, self).__init__(queryset, None,371 cache_choices, required, widget, label, initial, help_text,372 *args, **kwargs)373 374 def clean(self, value):375 if self.required and not value:376 raise ValidationError(self.error_messages['required'])377 elif not self.required and not value:378 return []379 if not isinstance(value, (list, tuple)):380 raise ValidationError(self.error_messages['list'])381 final_values = []382 for val in value:383 try:384 obj = self.queryset.get(pk=val)385 except self.queryset.model.DoesNotExist:386 raise ValidationError(self.error_messages['invalid_choice'] % val)387 else:388 final_values.append(obj)389 return final_values390 391 # Model-FormSet integration ###################################################392 393 def initial_data(instance, fields=None):394 """395 Return a dictionary from data in ``instance`` that is suitable for396 use as a ``Form`` constructor's ``initial`` argument.397 398 Provide ``fields`` to specify the names of specific fields to return.399 All field values in the instance will be returned if ``fields`` is not400 provided.401 """402 # avoid a circular import403 from django.db.models.fields.related import ManyToManyField404 opts = instance._meta405 initial = {}406 for f in opts.fields + opts.many_to_many:407 if not f.editable:408 continue409 if fields and not f.name in fields:410 continue411 if isinstance(f, ManyToManyField):412 # MultipleChoiceWidget needs a list of ints, not object instances.413 initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]414 else:415 initial[f.name] = f.value_from_object(instance)416 return initial417 418 class BaseModelFormSet(BaseFormSet):419 """420 A ``FormSet`` for editing a queryset and/or adding new objects to it.421 """422 model = None423 queryset = None424 425 def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None):426 304 kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 427 self.queryset = qs 428 kwargs['initial'] = [initial_data(obj) for obj in qs] 305 kwargs['initial'] = [model_to_dict(obj) for obj in self.get_queryset()] 429 306 super(BaseModelFormSet, self).__init__(**kwargs) 307 308 def get_queryset(self): 309 if self.queryset is not None: 310 return self.queryset 311 return self.model._default_manager.get_query_set() 430 312 431 313 def save_new(self, form, commit=True): … … 433 315 return save_instance(form, self.model(), commit=commit) 434 316 435 def save_ instance(self, form, instance, commit=True):317 def save_existing(self, form, instance, commit=True): 436 318 """Saves and returns an existing model instance for the given form.""" 437 319 return save_instance(form, instance, commit=commit) … … 444 326 445 327 def save_existing_objects(self, commit=True): 446 if not self. queryset:328 if not self.get_queryset(): 447 329 return [] 448 330 # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk 449 331 existing_objects = {} 450 for obj in self. queryset:332 for obj in self.get_queryset(): 451 333 existing_objects[obj.pk] = obj 452 334 saved_instances = [] 453 for form in self. change_forms:335 for form in self.initial_forms: 454 336 obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]] 455 if self. deletable and form.cleaned_data[DELETION_FIELD_NAME]:337 if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: 456 338 obj.delete() 457 339 else: 458 saved_instances.append(self.save_ instance(form, obj, commit=commit))340 saved_instances.append(self.save_existing(form, obj, commit=commit)) 459 341 return saved_instances 460 342 461 343 def save_new_objects(self, commit=True): 462 344 new_objects = [] 463 for form in self. add_forms:464 if form.is_empty():345 for form in self.extra_forms: 346 if not form.has_changed(): 465 347 continue 466 348 # If someone has marked an add form for deletion, don't save the 467 349 # object. At some point it would be nice if we didn't display 468 350 # the deletion widget for add forms. 469 if self. deletable and form.cleaned_data[DELETION_FIELD_NAME]:351 if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: 470 352 continue 471 353 new_objects.append(self.save_new(form, commit=commit)) … … 478 360 super(BaseModelFormSet, self).add_fields(form, index) 479 361 480 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 481 formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): 482 """ 483 Returns a FormSet class for the given Django model class. This FormSet 484 will contain change forms for every instance of the given model as well 485 as the number of add forms specified by ``extra``. 486 487 This is essentially the same as ``formset_for_queryset``, but automatically 488 uses the model's default manager to determine the queryset. 489 """ 490 form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) 491 FormSet = formset_for_form(form, formset, extra, orderable, deletable) 362 # XXX: Use at your own risk. This API *will* change. 363 def _modelformset_factory(model, form=BaseModelForm, formfield_callback=lambda f: f.formfield(), 364 formset=BaseModelFormSet, 365 extra=1, can_delete=False, can_order=False, 366 fields=None, exclude=None): 367 """ 368 Returns a FormSet class for the given Django model class. 369 """ 370 form = _modelform_factory(model, form=form, fields=fields, exclude=exclude, 371 formfield_callback=formfield_callback) 372 FormSet = _formset_factory(form, formset, extra=extra, can_order=can_order, can_delete=can_delete) 492 373 FormSet.model = model 493 374 return FormSet 494 375 495 class InlineFormset(BaseModelFormSet): 376 377 # InlineFormSets ############################################################# 378 379 class BaseInlineFormset(BaseModelFormSet): 496 380 """A formset for child objects related to a parent.""" 497 def __init__(self, instance, data=None, files=None):381 def __init__(self, data=None, files=None, instance=None): 498 382 from django.db.models.fields.related import RelatedObject 499 383 self.instance = instance 500 384 # is there a better way to get the object descriptor? 501 385 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() 502 qs = self.get_queryset() 503 super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name) 386 super(BaseInlineFormset, self).__init__(data, files, prefix=self.rel_name) 504 387 505 388 def get_queryset(self): … … 516 399 return save_instance(form, new_obj, commit=commit) 517 400 518 def get_foreign_key(parent_model, model, fk_name=None):401 def _get_foreign_key(parent_model, model, fk_name=None): 519 402 """ 520 403 Finds and returns the ForeignKey from model to parent if there is one. … … 543 426 return fk 544 427 545 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 428 429 # XXX: This API *will* change. Use at your own risk. 430 def _inlineformset_factory(parent_model, model, form=BaseModelForm, fk_name=None, 431 fields=None, exclude=None, 432 extra=3, can_order=False, can_delete=True, 433 formfield_callback=lambda f: f.formfield()): 546 434 """ 547 435 Returns an ``InlineFormset`` for the given kwargs. … … 550 438 to ``parent_model``. 551 439 """ 552 fk = get_foreign_key(parent_model, model, fk_name=fk_name)440 fk = _get_foreign_key(parent_model, model, fk_name=fk_name) 553 441 # let the formset handle object deletion by default 554 FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 555 formfield_callback=formfield_callback, 556 extra=extra, orderable=orderable, 557 deletable=deletable) 558 # HACK: remove the ForeignKey to the parent from every form 559 # This should be done a line above before we pass 'fields' to formset_for_model 560 # an 'omit' argument would be very handy here 561 try: 562 del FormSet.form_class.base_fields[fk.name] 563 except KeyError: 564 pass 442 443 if exclude is not None: 444 exclude.append(fk.name) 445 else: 446 exclude = [fk.name] 447 FormSet = _modelformset_factory(model, form=form, 448 formfield_callback=formfield_callback, 449 formset=BaseInlineFormset, 450 extra=extra, can_delete=can_delete, can_order=can_order, 451 fields=fields, exclude=exclude) 565 452 FormSet.fk = fk 566 453 return FormSet 454 455 456 # Fields ##################################################################### 457 458 class QuerySetIterator(object): 459 def __init__(self, queryset, empty_label, cache_choices): 460 self.queryset = queryset 461 self.empty_label = empty_label 462 self.cache_choices = cache_choices 463 464 def __iter__(self): 465 if self.empty_label is not None: 466 yield (u"", self.empty_label) 467 for obj in self.queryset: 468 yield (obj.pk, smart_unicode(obj)) 469 # Clear the QuerySet cache if required. 470 if not self.cache_choices: 471 self.queryset._result_cache = None 472 473 class ModelChoiceField(ChoiceField): 474 """A ChoiceField whose choices are a model QuerySet.""" 475 # This class is a subclass of ChoiceField for purity, but it doesn't 476 # actually use any of ChoiceField's implementation. 477 default_error_messages = { 478 'invalid_choice': _(u'Select a valid choice. That choice is not one of' 479 u' the available choices.'), 480 } 481 482 def __init__(self, queryset, empty_label=u"---------", cache_choices=False, 483 required=True, widget=Select, label=None, initial=None, 484 help_text=None, *args, **kwargs): 485 self.empty_label = empty_label 486 self.cache_choices = cache_choices 487 # Call Field instead of ChoiceField __init__() because we don't need 488 # ChoiceField.__init__(). 489 Field.__init__(self, required, widget, label, initial, help_text, 490 *args, **kwargs) 491 self.queryset = queryset 492 493 def _get_queryset(self): 494 return self._queryset 495 496 def _set_queryset(self, queryset): 497 self._queryset = queryset 498 self.widget.choices = self.choices 499 500 queryset = property(_get_queryset, _set_queryset) 501 502 def _get_choices(self): 503 # If self._choices is set, then somebody must have manually set 504 # the property self.choices. In this case, just return self._choices. 505 if hasattr(self, '_choices'): 506 return self._choices 507 # Otherwise, execute the QuerySet in self.queryset to determine the 508 # choices dynamically. Return a fresh QuerySetIterator that has not 509 # been consumed. Note that we're instantiating a new QuerySetIterator 510 # *each* time _get_choices() is called (and, thus, each time 511 # self.choices is accessed) so that we can ensure the QuerySet has not 512 # been consumed. 513 return QuerySetIterator(self.queryset, self.empty_label, 514 self.cache_choices) 515 516 def _set_choices(self, value): 517 # This method is copied from ChoiceField._set_choices(). It's necessary 518 # because property() doesn't allow a subclass to overwrite only 519 # _get_choices without implementing _set_choices. 520 self._choices = self.widget.choices = list(value) 521 522 choices = property(_get_choices, _set_choices) 523 524 def clean(self, value): 525 Field.clean(self, value) 526 if value in EMPTY_VALUES: 527 return None 528 try: 529 value = self.queryset.get(pk=value) 530 except self.queryset.model.DoesNotExist: 531 raise ValidationError(self.error_messages['invalid_choice']) 532 return value 533 534 class ModelMultipleChoiceField(ModelChoiceField): 535 """A MultipleChoiceField whose choices are a model QuerySet.""" 536 hidden_widget = MultipleHiddenInput 537 default_error_messages = { 538 'list': _(u'Enter a list of values.'), 539 'invalid_choice': _(u'Select a valid choice. %s is not one of the' 540 u' available choices.'), 541 } 542 543 def __init__(self, queryset, cache_choices=False, required=True, 544 widget=SelectMultiple, label=None, initial=None, 545 help_text=None, *args, **kwargs): 546 super(ModelMultipleChoiceField, self).__init__(queryset, None, 547 cache_choices, required, widget, label, initial, help_text, 548 *args, **kwargs) 549 550 def clean(self, value): 551 if self.required and not value: 552 raise ValidationError(self.error_messages['required']) 553 elif not self.required and not value: 554 return [] 555 if not isinstance(value, (list, tuple)): 556 raise ValidationError(self.error_messages['list']) 557 final_values = [] 558 for val in value: 559 try: 560 obj = self.queryset.get(pk=val) 561 except self.queryset.model.DoesNotExist: 562 raise ValidationError(self.error_messages['invalid_choice'] % val) 563 else: 564 final_values.append(obj) 565 return final_values django/branches/newforms-admin/django/newforms/widgets.py
r7121 r7270 165 165 """ 166 166 return data.get(name, None) 167 168 def is_empty(self, value): 169 """ 170 Given a dictionary of data and this widget's name, return True if the 171 widget data is empty or False when not empty. 172 """ 173 if value not in (None, ''): 174 return False 175 return True 167 168 def _has_changed(self, initial, data): 169 """ 170 Return True if data differs from initial. 171 """ 172 # For purposes of seeing whether something has changed, None is 173 # the same as an empty string, if the data or inital value we get 174 # is None, replace it w/ u''. 175 data_value = data or u'' 176 initial_value = initial or u'' 177 if force_unicode(initial_value) != force_unicode(data_value): 178 return True 179 return False 176 180 177 181 def id_for_label(self, id_): … … 310 314 return False 311 315 return super(CheckboxInput, self).value_from_datadict(data, files, name) 312 313 def is_empty(self, value):314 # this widget will always either be True or False, so always returnthe315 # opposite value so False values will make the form empty316 return not value316 317 def _has_changed(self, initial, data): 318 # Sometimes data or initial could be None or u'' which should be the 319 # same thing as False. 320 return bool(initial) != bool(data) 317 321 318 322 class Select(Widget): … … 357 361 value = data.get(name, None) 358 362 return {u'2': True, u'3': False, True: True, False: False}.get(value, None) 359 360 def is_empty(self, value): 361 # this widget will always either be True, False or None, so always 362 # return the opposite value so False and None values will make the 363 # form empty. 364 return not value 363 364 def _has_changed(self, initial, data): 365 # Sometimes data or initial could be None or u'' which should be the 366 # same thing as False. 367 return bool(initial) != bool(data) 365 368 366 369 class SelectMultiple(Widget): … … 560 563 return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)] 561 564 562 def is_empty(self, value): 563 for widget, val in zip(self.widgets, value): 564 if not widget.is_empty(val): 565 def _has_changed(self, initial, data): 566 if initial is None: 567 initial = [u'' for x in range(0, len(data))] 568 for widget, initial, data in zip(self.widgets, initial, data): 569 if not widget._has_changed(initial, data): 565 570 return False 566 571 return True django/branches/newforms-admin/tests/modeltests/model_formsets/models.py
r6839 r7270 17 17 __test__ = {'API_TESTS': """ 18 18 19 >>> from django.newforms.models import formset_for_model19 >>> from django.newforms.models import _modelformset_factory 20 20 21 21 >>> qs = Author.objects.all() 22 >>> AuthorFormSet = formset_for_model(Author, extra=3)23 24 >>> formset = AuthorFormSet(q s)22 >>> AuthorFormSet = _modelformset_factory(Author, extra=3) 23 24 >>> formset = AuthorFormSet(queryset=qs) 25 25 >>> for form in formset.forms: 26 26 ... print form.as_p() … … 30 30 31 31 >>> data = { 32 ... 'form-COUNT': '3', 32 ... 'form-TOTAL_FORMS': '3', # the number of forms rendered 33 ... 'form-INITIAL_FORMS': '0', # the number of forms with initial data 33 34 ... 'form-0-name': 'Charles Baudelaire', 34 35 ... 'form-1-name': 'Arthur Rimbaud', … … 36 37 ... } 37 38 38 >>> formset = AuthorFormSet( qs, data=data)39 >>> formset = AuthorFormSet(data=data, queryset=qs) 39 40 >>> formset.is_valid() 40 41 True … … 55 56 56 57 >>> qs = Author.objects.order_by('name') 57 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False)58 59 >>> formset = AuthorFormSet(q s)58 >>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=False) 59 60 >>> formset = AuthorFormSet(queryset=qs) 60 61 >>> for form in formset.forms: 61 62 ... print form.as_p() … … 66 67 67 68 >>> data = { 68 ... 'form-COUNT': '3', 69 ... 'form-TOTAL_FORMS': '3', # the number of forms rendered 70 ... 'form-INITIAL_FORMS': '2', # the number of forms with initial data 69 71 ... 'form-0-id': '2', 70 72 ... 'form-0-name': 'Arthur Rimbaud', … … 74 76 ... } 75 77 76 >>> formset = AuthorFormSet( qs, data=data)78 >>> formset = AuthorFormSet(data=data, queryset=qs) 77 79 >>> formset.is_valid() 78 80 True … … 92 94 93 95 >>> qs = Author.objects.order_by('name') 94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)95 96 >>> formset = AuthorFormSet(q s)96 >>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=True) 97 98 >>> formset = AuthorFormSet(queryset=qs) 97 99 >>> for form in formset.forms: 98 100 ... print form.as_p() … … 107 109 108 110 >>> data = { 109 ... 'form-COUNT': '4', 111 ... 'form-TOTAL_FORMS': '4', # the number of forms rendered 112 ... 'form-INITIAL_FORMS': '3', # the number of forms with initial data 110 113 ... 'form-0-id': '2', 111 114 ... 'form-0-name': 'Arthur Rimbaud', … … 118 121 ... } 119 122 120 >>> formset = AuthorFormSet( qs, data=data)123 >>> formset = AuthorFormSet(data=data, queryset=qs) 121 124 >>> formset.is_valid() 122 125 True … … 132 135 133 136 137 # Inline Formsets ############################################################ 138 134 139 We can also create a formset that is tied to a parent model. This is how the 135 140 admin system's edit inline functionality works. 136 141 137 >>> from django.newforms.models import inline_formset138 139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)142 >>> from django.newforms.models import _inlineformset_factory 143 144 >>> AuthorBooksFormSet = _inlineformset_factory(Author, Book, can_delete=False, extra=3) 140 145 >>> author = Author.objects.get(name='Charles Baudelaire') 141 146 142 >>> formset = AuthorBooksFormSet( author)147 >>> formset = AuthorBooksFormSet(instance=author) 143 148 >>> for form in formset.forms: 144 149 ... print form.as_p() … … 148 153 149 154 >>> data = { 150 ... 'book_set-COUNT': '3', 155 ... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered 156 ... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data 151 157 ... 'book_set-0-title': 'Les Fleurs du Mal', 152 158 ... 'book_set-1-title': '', … … 154 160 ... } 155 161 156 >>> formset = AuthorBooksFormSet( author, data=data)162 >>> formset = AuthorBooksFormSet(data, instance=author) 157 163 >>> formset.is_valid() 158 164 True … … 170 176 book. 171 177 172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)178 >>> AuthorBooksFormSet = _inlineformset_factory(Author, Book, can_delete=False, extra=2) 173 179 >>> author = Author.objects.get(name='Charles Baudelaire') 174 180 175 >>> formset = AuthorBooksFormSet( author)181 >>> formset = AuthorBooksFormSet(instance=author) 176 182 >>> for form in formset.forms: 177 183 ... print form.as_p() … … 181 187 182 188 >>> data = { 183 ... 'book_set-COUNT': '3', 189 ... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered 190 ... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data 184 191 ... 'book_set-0-id': '1', 185 192 ... 'book_set-0-title': 'Les Fleurs du Mal', … … 188 195 ... } 189 196 190 >>> formset = AuthorBooksFormSet( author, data=data)197 >>> formset = AuthorBooksFormSet(data, instance=author) 191 198 >>> formset.is_valid() 192 199 True django/branches/newforms-admin/tests/regressiontests/forms/formsets.py
r6419 r7270 1 1 # -*- coding: utf-8 -*- 2 formset_tests = """2 tests = """ 3 3 # Basic FormSet creation and usage ############################################ 4 4 5 5 FormSet allows us to use multiple instance of the same form on 1 page. For now, 6 the best way to create a FormSet is by using the formset_for_formfunction.6 the best way to create a FormSet is by using the _formset_factory function. 7 7 8 8 >>> from django.newforms import Form, CharField, IntegerField, ValidationError 9 >>> from django.newforms.formsets import formset_for_form, BaseFormSet9 >>> from django.newforms.formsets import _formset_factory, BaseFormSet 10 10 11 11 >>> class Choice(Form): … … 13 13 ... votes = IntegerField() 14 14 15 >>> ChoiceFormSet = formset_for_form(Choice) 16 15 >>> ChoiceFormSet = _formset_factory(Choice) 17 16 18 17 A FormSet constructor takes the same arguments as Form. Let's create a FormSet … … 21 20 22 21 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices') 23 >>> for form in formset.forms: 24 ... print form.as_ul() 25 <li>Choice: <input type="text" name="choices-0-choice" /></li> 26 <li>Votes: <input type="text" name="choices-0-votes" /></li> 22 >>> print formset 23 <input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /> 24 <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> 25 <tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr> 26 27 27 28 28 On thing to note is that there needs to be a special value in the data. This … … 30 30 many forms it needs to clean and validate. You could use javascript to create 31 31 new forms on the client side, but they won't get validated unless you increment 32 the COUNT field appropriately. 33 34 >>> data = { 35 ... 'choices-COUNT': '1', # the number of forms rendered 32 the TOTAL_FORMS field appropriately. 33 34 >>> data = { 35 ... 'choices-TOTAL_FORMS': '1', # the number of forms rendered 36 ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 36 37 ... 'choices-0-choice': 'Calexico', 37 38 ... 'choices-0-votes': '100', … … 46 47 >>> formset.is_valid() 47 48 True 48 >>> formset.cleaned_data49 >>> [form.cleaned_data for form in formset.forms] 49 50 [{'votes': 100, 'choice': u'Calexico'}] 50 51 … … 58 59 59 60 >>> data = { 60 ... 'choices-COUNT': '1', # the number of forms rendered 61 ... 'choices-TOTAL_FORMS': '1', # the number of forms rendered 62 ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 61 63 ... 'choices-0-choice': 'Calexico', 62 64 ... 'choices-0-votes': '', … … 68 70 >>> formset.errors 69 71 [{'votes': [u'This field is required.']}] 70 71 Like a Form instance, cleaned_data won't exist if the formset wasn't validated.72 73 >>> formset.cleaned_data74 Traceback (most recent call last):75 ...76 AttributeError: 'ChoiceFormSet' object has no attribute 'cleaned_data'77 72 78 73 … … 94 89 95 90 >>> data = { 96 ... 'choices-COUNT': '2', # the number of forms rendered 91 ... 'choices-TOTAL_FORMS': '2', # the number of forms rendered 92 ... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data 97 93 ... 'choices-0-choice': 'Calexico', 98 94 ... 'choices-0-votes': '100', … … 104 100 >>> formset.is_valid() 105 101 True 106 >>> formset.cleaned_data107 [{'votes': 100, 'choice': u'Calexico'} ]102 >>> [form.cleaned_data for form in formset.forms] 103 [{'votes': 100, 'choice': u'Calexico'}, {}] 108 104 109 105 But the second form was blank! Shouldn't we get some errors? No. If we display … … 114 110 115 111 >>> data = { 116 ... 'choices-COUNT': '2', # the number of forms rendered 112 ... 'choices-TOTAL_FORMS': '2', # the number of forms rendered 113 ... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data 117 114 ... 'choices-0-choice': 'Calexico', 118 115 ... 'choices-0-votes': '100', … … 126 123 >>> formset.errors 127 124 [{}, {'votes': [u'This field is required.']}] 128 129 125 130 126 If we delete data that was pre-filled, we should get an error. Simply removing … … 133 129 134 130 >>> data = { 135 ... 'choices-COUNT': '2', # the number of forms rendered 131 ... 'choices-TOTAL_FORMS': '2', # the number of forms rendered 132 ... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data 136 133 ... 'choices-0-choice': '', # deleted value 137 134 ... 'choices-0-votes': '', # deleted value … … 144 141 False 145 142 >>> formset.errors 146 [{'votes': [u'This field is required.'], 'choice': [u'This field is required.']} ]143 [{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}] 147 144 148 145 … … 150 147 151 148 We can also display more than 1 empty form at a time. To do so, pass a 152 num_extra argument to formset_for_form.153 154 >>> ChoiceFormSet = formset_for_form(Choice, num_extra=3)149 extra argument to _formset_factory. 150 151 >>> ChoiceFormSet = _formset_factory(Choice, extra=3) 155 152 156 153 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices') … … 169 166 170 167 >>> data = { 171 ... 'choices-COUNT': '3', # the number of forms rendered 168 ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered 169 ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 172 170 ... 'choices-0-choice': '', 173 171 ... 'choices-0-votes': '', … … 181 179 >>> formset.is_valid() 182 180 True 183 >>> formset.cleaned_data184 [ ]181 >>> [form.cleaned_data for form in formset.forms] 182 [{}, {}, {}] 185 183 186 184 … … 188 186 189 187 >>> data = { 190 ... 'choices-COUNT': '3', # the number of forms rendered 188 ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered 189 ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 191 190 ... 'choices-0-choice': 'Calexico', 192 191 ... 'choices-0-votes': '100', … … 200 199 >>> formset.is_valid() 201 200 True 202 >>> formset.cleaned_data203 [{'votes': 100, 'choice': u'Calexico'} ]201 >>> [form.cleaned_data for form in formset.forms] 202 [{'votes': 100, 'choice': u'Calexico'}, {}, {}] 204 203 205 204 … … 207 206 208 207 >>> data = { 209 ... 'choices-COUNT': '3', # the number of forms rendered 208 ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered 209 ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 210 210 ... 'choices-0-choice': 'Calexico', 211 211 ... 'choices-0-votes': '100', … … 220 220 False 221 221 >>> formset.errors 222 [{}, {'votes': [u'This field is required.']} ]223 224 225 The num_extra argument also works when the formset is pre-filled with initial222 [{}, {'votes': [u'This field is required.']}, {}] 223 224 225 The extra argument also works when the formset is pre-filled with initial 226 226 data. 227 227 … … 240 240 241 241 242 If we try to skip a form, even if it was initially displayed as blank, we will243 get an error.244 245 >>> data = {246 ... 'choices-COUNT': '3', # the number of forms rendered247 ... 'choices-0-choice': 'Calexico',248 ... 'choices-0-votes': '100',249 ... 'choices-1-choice': '',250 ... 'choices-1-votes': '',251 ... 'choices-2-choice': 'The Decemberists',252 ... 'choices-2-votes': '12',253 ... 'choices-3-choice': '',254 ... 'choices-3-votes': '',255 ... }256 257 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')258 >>> formset.is_valid()259 False260 >>> formset.errors261 [{}, {'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]262 263 264 242 # FormSets with deletion ###################################################### 265 243 266 244 We can easily add deletion ability to a FormSet with an agrument to 267 formset_for_form. This will add a boolean field to each form instance. When 268 that boolean field is True, the cleaned data will be in formset.deleted_data 269 rather than formset.cleaned_data 270 271 >>> ChoiceFormSet = formset_for_form(Choice, deletable=True) 245 _formset_factory. This will add a boolean field to each form instance. When 246 that boolean field is True, the form will be in formset.deleted_forms 247 248 >>> ChoiceFormSet = _formset_factory(Choice, can_delete=True) 272 249 273 250 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] … … 289 266 290 267 >>> data = { 291 ... 'choices-COUNT': '3', # the number of forms rendered 268 ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered 269 ... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data 292 270 ... 'choices-0-choice': 'Calexico', 293 271 ... 'choices-0-votes': '100', … … 304 282 >>> formset.is_valid() 305 283 True 306 >>> formset.cleaned_data307 [{'votes': 100, 'DELETE': False, 'choice': u'Calexico'} ]308 >>> formset.deleted_data284 >>> [form.cleaned_data for form in formset.forms] 285 [{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}] 286 >>> [form.cleaned_data for form in formset.deleted_forms] 309 287 [{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}] 310 288 289 311 290 # FormSets with ordering ###################################################### 312 291 313 292 We can also add ordering ability to a FormSet with an agrument to 314 formset_for_form. This will add a integer field to each form instance. When315 form validation succeeds, formset.cleaned_datawill have the data in the correct293 _formset_factory. This will add a integer field to each form instance. When 294 form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct 316 295 order specified by the ordering fields. If a number is duplicated in the set 317 296 of ordering fields, for instance form 0 and form 3 are both marked as 1, then … … 319 298 something at the front of the list, you'd need to set it's order to 0. 320 299 321 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True)300 >>> ChoiceFormSet = _formset_factory(Choice, can_order=True) 322 301 323 302 >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] … … 333 312 <li>Choice: <input type="text" name="choices-2-choice" /></li> 334 313 <li>Votes: <input type="text" name="choices-2-votes" /></li> 335 <li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li> 336 337 >>> data = { 338 ... 'choices-COUNT': '3', # the number of forms rendered 314 <li>Order: <input type="text" name="choices-2-ORDER" /></li> 315 316 >>> data = { 317 ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered 318 ... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data 339 319 ... 'choices-0-choice': 'Calexico', 340 320 ... 'choices-0-votes': '100', … … 351 331 >>> formset.is_valid() 352 332 True 353 >>> for cleaned_data in formset.cleaned_data:354 ... print cleaned_data333 >>> for form in formset.ordered_forms: 334 ... print form.cleaned_data 355 335 {'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'} 356 336 {'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} 357 337 {'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} 358 338 339 Ordering fields are allowed to be left blank, and if they *are* left blank, 340 they will be sorted below everything else. 341 342 >>> data = { 343 ... 'choices-TOTAL_FORMS': '4', # the number of forms rendered 344 ... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data 345 ... 'choices-0-choice': 'Calexico', 346 ... 'choices-0-votes': '100', 347 ... 'choices-0-ORDER': '1', 348 ... 'choices-1-choice': 'Fergie', 349 ... 'choices-1-votes': '900', 350 ... 'choices-1-ORDER': '2', 351 ... 'choices-2-choice': 'The Decemberists', 352 ... 'choices-2-votes': '500', 353 ... 'choices-2-ORDER': '', 354 ... 'choices-3-choice': 'Basia Bulat', 355 ... 'choices-3-votes': '50', 356 ... 'choices-3-ORDER': '', 357 ... } 358 359 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 360 >>> formset.is_valid() 361 True 362 >>> for form in formset.ordered_forms: 363 ... print form.cleaned_data 364 {'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} 365 {'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} 366 {'votes': 500, 'ORDER': None, 'choice': u'The Decemberists'} 367 {'votes': 50, 'ORDER': None, 'choice': u'Basia Bulat'} 368 369 359 370 # FormSets with ordering + deletion ########################################### 360 371 361 372 Let's try throwing ordering and deletion into the same form. 362 373 363 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True)374 >>> ChoiceFormSet = _formset_factory(Choice, can_order=True, can_delete=True) 364 375 365 376 >>> initial = [ … … 385 396 <li>Choice: <input type="text" name="choices-3-choice" /></li> 386 397 <li>Votes: <input type="text" name="choices-3-votes" /></li> 387 <li>Order: <input type="text" name="choices-3-ORDER" value="4"/></li>398 <li>Order: <input type="text" name="choices-3-ORDER" /></li> 388 399 <li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li> 389 400 … … 391 402 392 403 >>> data = { 393 ... 'choices-COUNT': '4', # the number of forms rendered 404 ... 'choices-TOTAL_FORMS': '4', # the number of forms rendered 405 ... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data 394 406 ... 'choices-0-choice': 'Calexico', 395 407 ... 'choices-0-votes': '100', … … 406 418 ... 'choices-3-choice': '', 407 419 ... 'choices-3-votes': '', 408 ... 'choices-3-ORDER': ' 4',420 ... 'choices-3-ORDER': '', 409 421 ... 'choices-3-DELETE': '', 410 422 ... } … … 413 425 >>> formset.is_valid() 414 426 True 415 >>> for cleaned_data in formset.cleaned_data:416 ... print cleaned_data427 >>> for form in formset.ordered_forms: 428 ... print form.cleaned_data 417 429 {'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'} 418 430 {'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'} 419 >>> formset.deleted_data431 >>> [form.cleaned_data for form in formset.deleted_forms] 420 432 [{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}] 421 433 … … 434 446 435 447 >>> class FavoriteDrinksFormSet(BaseFormSet): 436 ... form _class= FavoriteDrinkForm437 ... num_extra = 2438 ... orderable= False439 ... deletable = False448 ... form = FavoriteDrinkForm 449 ... extra = 2 450 ... can_order = False 451 ... can_delete = False 440 452 ... 441 453 ... def clean(self): … … 445 457 ... raise ValidationError('You may only specify a drink once.') 446 458 ... seen_drinks.append(drink['name']) 447 ... return self.cleaned_data448 459 ... 449 460 … … 451 462 452 463 >>> data = { 453 ... 'drinks-COUNT': '2', 464 ... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered 465 ... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 454 466 ... 'drinks-0-name': 'Gin and Tonic', 455 467 ... 'drinks-1-name': 'Gin and Tonic', … … 471 483 472 484 >>> data = { 473 ... 'drinks-COUNT': '2', 485 ... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered 486 ... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 474 487 ... 'drinks-0-name': 'Gin and Tonic', 475 488 ... 'drinks-1-name': 'Bloody Mary', django/branches/newforms-admin/tests/regressiontests/forms/forms.py
r6776 r7270 1604 1604 <input type="submit" /> 1605 1605 </form> 1606 1607 1608 # The empty_permitted attribute ############################################## 1609 1610 Sometimes (pretty much in formsets) we want to allow a form to pass validation 1611 if it is completely empty. We can accomplish this by using the empty_permitted 1612 agrument to a form constructor. 1613 1614 >>> class SongForm(Form): 1615 ... artist = CharField() 1616 ... name = CharField() 1617 1618 First let's show what happens id empty_permitted=False (the default): 1619 1620 >>> data = {'artist': '', 'song': ''} 1621 1622 >>> form = SongForm(data, empty_permitted=False) 1623 >>> form.is_valid() 1624 False 1625 >>> form.errors 1626 {'name': [u'This field is required.'], 'artist': [u'This field is required.']} 1627 >>> form.cleaned_data 1628 Traceback (most recent call last): 1629 ... 1630 AttributeError: 'SongForm' object has no attribute 'cleaned_data' 1631 1632 1633 Now let's show what happens when empty_permitted=True and the form is empty. 1634 1635 >>> form = SongForm(data, empty_permitted=True) 1636 >>> form.is_valid() 1637 True 1638 >>> form.errors 1639 {} 1640 >>> form.cleaned_data 1641 {} 1642 1643 But if we fill in data for one of the fields, the form is no longer empty and 1644 the whole thing must pass validation. 1645 1646 >>> data = {'artist': 'The Doors', 'song': ''} 1647 >>> form = SongForm(data, empty_permitted=False) 1648 >>> form.is_valid() 1649 False 1650 >>> form.errors 1651 {'name': [u'This field is required.']} 1652 >>> form.cleaned_data 1653 Traceback (most recent call last): 1654 ... 1655 AttributeError: 'SongForm' object has no attribute 'cleaned_data' 1656 1606 1657 """ django/branches/newforms-admin/tests/regressiontests/forms/tests.py
r6864 r7270 27 27 from util import tests as util_tests 28 28 from widgets import tests as widgets_tests 29 from formsets import formset_tests29 from formsets import tests as formset_tests 30 30 from media import media_tests 31 31 django/branches/newforms-admin/tests/regressiontests/forms/widgets.py
r6837 r7270 293 293 False 294 294 295 The CheckboxInput widget will always be empty when there is a False value296 >>> w.is_empty(False)297 True298 >>> w.is_empty(True)299 False300 301 295 # Select Widget ############################################################### 302 296 … … 459 453 <option value="3" selected="selected">No</option> 460 454 </select> 461 462 The NullBooleanSelect widget will always be empty when Unknown or No is selected463 as its value. This is to stay compliant with the CheckboxInput behavior464 >>> w.is_empty(False)465 True466 >>> w.is_empty(None)467 True468 >>> w.is_empty(True)469 False470 455 471 456 """ + \ … … 911 896 u'<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />' 912 897 913 The MultiWidget will be empty only when all widgets are considered empty.914 >>> w.is_empty(['john', 'lennon'])915 False916 >>> w.is_empty(['john', ''])917 False918 >>> w.is_empty(['', ''])919 True920 >>> w.is_empty([None, None])921 True922 923 898 # SplitDateTimeWidget ######################################################### 924 899 django/branches/newforms-admin/tests/regressiontests/inline_formsets/models.py
r6301 r7270 16 16 __test__ = {'API_TESTS': """ 17 17 18 >>> from django.newforms.models import inline_formset18 >>> from django.newforms.models import _inlineformset_factory 19 19 20 20 … … 22 22 for the inline formset, we should get an exception. 23 23 24 >>> ifs = inline_formset(Parent, Child)24 >>> ifs = _inlineformset_factory(Parent, Child) 25 25 Traceback (most recent call last): 26 26 ... … … 30 30 These two should both work without a problem. 31 31 32 >>> ifs = inline_formset(Parent, Child, fk_name='mother')33 >>> ifs = inline_formset(Parent, Child, fk_name='father')32 >>> ifs = _inlineformset_factory(Parent, Child, fk_name='mother') 33 >>> ifs = _inlineformset_factory(Parent, Child, fk_name='father') 34 34 35 35 … … 37 37 parent model, we should get an exception. 38 38 39 >>> ifs = inline_formset(Parent, Child, fk_name='school')39 >>> ifs = _inlineformset_factory(Parent, Child, fk_name='school') 40 40 Traceback (most recent call last): 41 41 ... … … 46 46 exception. 47 47 48 >>> ifs = inline_formset(Parent, Child, fk_name='test')48 >>> ifs = _inlineformset_factory(Parent, Child, fk_name='test') 49 49 Traceback (most recent call last): 50 50 ...
