Changeset 7270
- Timestamp:
- 03/17/08 12:55:16 (4 months 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 &nbs
