Ticket #6632: 02-newforms-inlines.2.diff
File 02-newforms-inlines.2.diff, 38.7 KB (added by , 17 years ago) |
---|
-
django/newforms/formsets.py
=== added file 'django/newforms/formsets.py'
1 from django.utils.encoding import StrAndUnicode 2 from django.utils.translation import ugettext as _ 3 4 from fields import BooleanField, IntegerField 5 from forms import Form, FormOptions 6 from widgets import HiddenInput 7 from util import ValidationError 8 import metaclassing 9 10 __all__ = ('BaseFormSet', 'FormSet') 11 12 # Special field names. 13 CHANGE_FORMS_COUNT_FIELD_NAME = 'CHANGE-FORMS-COUNT' 14 ALL_FORMS_COUNT_FIELD_NAME = 'ALL-FORMS-COUNT' 15 ORDERING_FIELD_NAME = 'ORDER' 16 DELETION_FIELD_NAME = 'DELETE' 17 18 class ManagementForm(Form): 19 """ 20 ``ManagementForm`` is used to keep track of how many form instances 21 are displayed on the page. If adding new forms via javascript, you should 22 increment the count field of this form as well. 23 """ 24 def __init__(self, *args, **kwargs): 25 self.base_fields[CHANGE_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 26 self.base_fields[ALL_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 27 super(ManagementForm, self).__init__(*args, **kwargs) 28 29 class FormSetOptions(FormOptions): 30 def __init__(self, options=None): 31 super(FormSetOptions, self).__init__(options) 32 # form 33 self.form = getattr(options, 'form', None) 34 self.base_form = getattr(options, 'base_form', Form) 35 # other options 36 self.deletable = getattr(options, 'deletable', False) 37 self.is_empty_fields = getattr(options, 'is_empty_fields', None) 38 self.is_empty_exclude = getattr(options, 'is_empty_exclude', None) 39 self.fieldset_attrs = getattr(options, 'fieldset_attrs', None) 40 self.fieldset_legend = getattr(options, 'fieldset_legend', None) 41 self.num_extra = getattr(options, 'num_extra', 1) 42 self.orderable = getattr(options, 'orderable', False) 43 self.output_type = getattr(options, 'output_type', 'tr') 44 45 class FormSetMetaclass(type): 46 def __new__(cls, name, bases, attrs): 47 new_class = type.__new__(cls, name, bases, attrs) 48 metaclassing.create_meta(new_class) 49 metaclassing.create_form_if_not_exists(new_class) 50 metaclassing.check_no_fieldsets_in_form(new_class) 51 return new_class 52 53 class BaseFormSet(StrAndUnicode): 54 """A collection of instances of the same Form class.""" 55 56 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None): 57 self.is_bound = data is not None or files is not None 58 self.data = data 59 self.files = files 60 self.auto_id = auto_id 61 self.prefix = prefix or 'form' 62 self.initial = initial 63 self._is_valid = None # Stores validation state after full_clean() has been called. 64 self._create_forms() 65 66 def _create_forms(self): 67 # TODO START - replace this ugly hack 68 for field in self._meta.form.base_fields.values(): 69 if hasattr(field, 'queryset'): 70 if field.cache_choices is False: 71 field.cache_choices = True 72 field.queryset._result_cache = None 73 field.queryset = field.queryset 74 field.cache_choices = False 75 # TODO END - replace this ugly hack 76 self._create_management_forms() 77 self._create_change_forms() 78 self._create_add_forms() 79 self.forms = self.change_forms + self.add_forms 80 81 def _create_management_forms(self): 82 # Initialization is different depending on whether we recieved data, initial, or nothing. 83 self.management_form = None 84 if self.data or self.files: 85 self.management_form = ManagementForm(self.data, self.files, auto_id=self.auto_id, prefix=self.prefix) 86 if self.management_form.is_valid(): 87 self.change_forms_count = self.management_form.cleaned_data[CHANGE_FORMS_COUNT_FIELD_NAME] 88 self.all_forms_count = self.management_form.cleaned_data[ALL_FORMS_COUNT_FIELD_NAME] 89 else: 90 # ManagementForm data is missing or has been tampered with." 91 self.management_form = None 92 if not self.management_form: 93 self.change_forms_count = self.initial and len(self.initial) or 0 94 self.all_forms_count = self.change_forms_count + self._meta.num_extra 95 management_form_initial = { 96 CHANGE_FORMS_COUNT_FIELD_NAME: self.change_forms_count, 97 ALL_FORMS_COUNT_FIELD_NAME: self.all_forms_count, 98 } 99 self.management_form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial=management_form_initial) 100 101 def _create_change_forms(self): 102 self.change_forms = [] 103 for i in range(0, self.change_forms_count): 104 kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 105 if self.data: 106 kwargs['data'] = self.data 107 if self.files: 108 kwargs['files'] = self.files 109 if self.initial: 110 kwargs['initial'] = self.initial[i] 111 change_form = self._meta.form(**kwargs) 112 self.add_fields(change_form, i) 113 self.change_forms.append(change_form) 114 115 def _create_add_forms(self): 116 self.add_forms = [] 117 for i in range(self.change_forms_count, self.all_forms_count): 118 kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 119 if self.data: 120 kwargs['data'] = self.data 121 if self.files: 122 kwargs['files'] = self.files 123 add_form = self._meta.form(**kwargs) 124 self.add_fields(add_form, i) 125 self.add_forms.append(add_form) 126 127 def __unicode__(self): 128 return getattr(self, 'as_%s' % self._meta.output_type)() 129 130 def add_prefix(self, index): 131 return '%s-%s' % (self.prefix, index) 132 133 def add_fields(self, form, index): 134 """A hook for adding extra fields on to each form instance.""" 135 if self._meta.orderable: 136 form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_('Order'), initial=index+1) 137 if self._meta.deletable: 138 form.fields[DELETION_FIELD_NAME] = BooleanField(label=_('Delete'), required=False) 139 140 def as_table(self): 141 "Returns this form rendered as HTML <tr>s." 142 return u'\n'.join(u'<table>\n%s\n</table>' % form.as_table() for form in [self.management_form] + self.forms) 143 144 def as_ul(self): 145 "Returns this form rendered as HTML <li>s." 146 return u'\n'.join(u'<ul>\n%s\n</ul>' % form.as_ul() for form in [self.management_form] + self.forms) 147 148 def as_p(self): 149 "Returns this form rendered as HTML <p>s." 150 return u'\n'.join(u'<div>\n%s\n</div>' % form.as_p() for form in [self.management_form] + self.forms) 151 152 def as_tr(self): 153 "Returns this form rendered as HTML <td>s." 154 output = [self.management_form.as_tr()] 155 if self.forms: 156 output.append(u'<tr>') 157 output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden) 158 output.append(u'</tr>') 159 output.extend(form.as_tr() for form in self.forms) 160 return '\n'.join(output) 161 162 def is_valid(self): 163 """ 164 Returns True if the formset (and its forms) have no errors. 165 """ 166 if self._is_valid is None: 167 self.full_clean() 168 return self._is_valid 169 170 def _get_errors(self): 171 """ 172 Returns list of ErrorDict for all forms. 173 """ 174 if self._is_valid is None: 175 self.full_clean() 176 return self._errors 177 errors = property(_get_errors) 178 179 def _get_non_form_errors(self): 180 """ 181 Returns an ErrorList of errors that aren't associated with a particular 182 form -- i.e., from formset.clean(). Returns an empty ErrorList if there 183 are none. 184 """ 185 if self._is_valid is None: 186 self.full_clean() 187 return self._non_form_errors 188 non_form_errors = property(_get_non_form_errors) 189 190 def full_clean(self): 191 """ 192 Cleans all of self.data and populates self._is_valid, self._errors, 193 self._no_form_errors, self.cleaned_data and self.deleted_data. 194 """ 195 self._is_valid = True # Assume the formset is valid until proven otherwise. 196 self._errors = [] 197 self._non_form_errors = self._meta.error_class() 198 if not self.is_bound: # Stop further processing. 199 return 200 self.cleaned_data = [] 201 self.deleted_data = [] 202 # Process change forms. 203 for form in self.change_forms: 204 if form.is_valid(): 205 if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 206 self.deleted_data.append(form.cleaned_data) 207 else: 208 self.cleaned_data.append(form.cleaned_data) 209 else: 210 self._is_valid = False 211 self._errors.append(form.errors) 212 # Process add forms in reverse so we can easily tell when the remaining 213 # ones should be required. 214 remaining_forms_required = False 215 add_errors = [] 216 for i in range(len(self.add_forms)-1, -1, -1): 217 form = self.add_forms[i] 218 # If an add form is empty, reset it so it won't have any errors. 219 if not remaining_forms_required and self.is_empty(form): 220 form.reset() 221 continue 222 else: 223 remaining_forms_required = True 224 if form.is_valid(): 225 self.cleaned_data.append(form.cleaned_data) 226 else: 227 self._is_valid = False 228 add_errors.append(form.errors) 229 add_errors.reverse() 230 self._errors.extend(add_errors) 231 # Sort cleaned_data if the formset is orderable. 232 if self._meta.orderable: 233 self.cleaned_data.sort(lambda x, y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) 234 # Give self.clean() a chance to do validation. 235 try: 236 self.cleaned_data = self.clean() 237 except ValidationError, e: 238 self._non_form_errors = self._meta.error_class(e.messages) 239 self._is_valid = False 240 # If there were errors, remove the cleaned_data and deleted_data attributes. 241 if not self._is_valid: 242 delattr(self, 'cleaned_data') 243 delattr(self, 'deleted_data') 244 245 def clean(self): 246 """ 247 Hook for doing any extra formset-wide cleaning after Form.clean() has 248 been called on every form. Any ValidationError raised by this method 249 will not be associated with a particular form; it will be accesible 250 via formset.non_form_errors() 251 """ 252 return self.cleaned_data 253 254 def is_empty(self, form=None): 255 """ 256 Returns True if the formset (including forms) or selected form 257 is empty. Otherwise, False. 258 """ 259 fields = self._meta.is_empty_fields and list(self._meta.is_empty_fields) or [] 260 exclude = self._meta.is_empty_exclude and list(self._meta.is_empty_exclude) or [] 261 if self._meta.orderable: 262 exclude.append(ORDERING_FIELD_NAME) 263 if form is None: 264 for form in self: 265 if not form.is_empty(fields, exclude): 266 return False 267 return True 268 else: 269 return form.is_empty(fields, exclude) 270 271 def reset(self): 272 """ 273 Resets the formset (including forms) to the state it was in 274 before data was passed to it. 275 """ 276 self.is_bound = False 277 self.data = {} 278 self.files = {} 279 self._is_valid = None 280 self._create_forms() 281 282 def is_multipart(self): 283 """ 284 Returns True if the formset needs to be multipart-encrypted, i.e. its 285 form has FileInput. Otherwise, False. 286 """ 287 if self.forms: 288 return self.forms[0].is_multipart() 289 else: 290 return False 291 292 class FormSet(BaseFormSet): 293 __metaclass__ = FormSetMetaclass 294 _options = FormSetOptions -
django/newforms/__init__.py
=== modified file 'django/newforms/__init__.py'
15 15 from fields import * 16 16 from forms import * 17 17 from models import * 18 from formsets import * -
django/newforms/forms.py
=== modified file 'django/newforms/forms.py'
29 29 self.fieldsets = getattr(options, 'fieldsets', None) 30 30 self.fields = getattr(options, 'fields', None) 31 31 self.exclude = getattr(options, 'exclude', None) 32 self.inlines = getattr(options, 'inlines', None) 32 33 # other options 33 34 self.error_class = getattr(options, 'error_class', ErrorList) 34 35 self.error_row_class = getattr(options, 'error_row_class', 'error') 35 36 self.hidden_row_class = getattr(options, 'hidden_row_class', 'hidden') 36 37 self.label_capfirst = getattr(options, 'label_capfirst', True) 37 38 self.label_suffix = getattr(options, 'label_suffix', ':') 39 self.output_type = getattr(options, 'output_type', 'table') 38 40 self.required_row_class = getattr(options, 'required_row_class', 'required') 39 41 self.validation_order = getattr(options, 'validation_order', None) 40 42 … … 45 47 metaclassing.create_declared_fields(new_class) 46 48 metaclassing.create_base_fields_pool_from_declared_fields(new_class) 47 49 metaclassing.create_base_fields_from_base_fields_pool(new_class) 50 metaclassing.create_fieldsets_if_inlines_exist(new_class) 48 51 return new_class 49 52 50 53 class BaseForm(StrAndUnicode): … … 52 55 # class is different than Form. See the comments by the Form class for more 53 56 # information. Any improvements to the form API should be made to *this* 54 57 # class, not to the Form class. 55 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None ):58 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, inlines=None): 56 59 self.is_bound = data is not None or files is not None 57 60 self.data = data or {} 58 61 self.files = files or {} 59 62 self.auto_id = auto_id 60 63 self.prefix = prefix 61 64 self.initial = initial or {} 62 self._errors = None # Stores the errors after clean() has been called. 65 self.inlines = inlines or [] # It is not responsibility of this class to create inlines. 66 self._is_valid = None # Stores validation state after full_clean() has been called. 63 67 64 68 # The base_fields class attribute is the *class-wide* definition of 65 69 # fields. Because a particular *instance* of the class might want to … … 69 73 self.fields = deepcopy(self.base_fields) 70 74 71 75 def __unicode__(self): 72 return self.as_table()76 return getattr(self, 'as_%s' % self._meta.output_type)() 73 77 74 78 def __iter__(self): 75 79 for name, field in self.fields.items(): … … 83 87 raise KeyError('Key %r not found in Form' % name) 84 88 return BoundField(self, field, name) 85 89 86 def _get_errors(self):87 "Returns an ErrorDict for the data provided for the form."88 if self._errors is None:89 self.full_clean()90 return self._errors91 errors = property(_get_errors)92 93 def is_valid(self):94 """95 Returns True if the form has no errors. Otherwise, False. If errors are96 being ignored, returns False.97 """98 return self.is_bound and not bool(self.errors)99 100 90 def has_fieldsets(self): 101 91 "Returns True if this form has fieldsets." 102 92 return bool(self._meta.fieldsets) … … 183 173 output.append(fieldset_end_html) 184 174 return u'\n'.join(output) 185 175 176 def _inline_html_output(self, fields, inline, is_first, is_last, fieldset_start_html, fieldset_end_html, legend_tag_html): 177 "Helper function for outputting HTML from a inline. Used by _html_output." 178 output = [] 179 if not is_first: 180 legend_tag = attrs = u'' 181 if inline._meta.fieldset_legend: 182 legend_tag = legend_tag_html % { 183 'legend': conditional_escape(force_unicode(inline._meta.fieldset_legend)), 184 } 185 if inline._meta.fieldset_attrs: 186 attrs = flatatt(inline._meta.fieldset_attrs) 187 output.append(fieldset_start_html % { 188 'legend_tag': legend_tag, 189 'attrs': attrs, 190 }) 191 output.append(unicode(inline)) 192 if not is_last: 193 output.append(fieldset_end_html) 194 return u'\n'.join(output) 195 186 196 def _hidden_fields_html_output(self, hidden_fields, hidden_fields_html): 187 197 "Helper function for outputting HTML from a hidden fields. Used by _html_output." 188 198 if self._meta.hidden_row_class: … … 210 220 if top_errors: 211 221 output.append(self._top_errors_html_output(top_errors, top_errors_html)) 212 222 if self.has_fieldsets(): 213 for i, fieldset in enumerate(self._meta.fieldsets):214 fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)223 inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines. 224 for i, fieldset_or_inline in enumerate(self._meta.fieldsets): 215 225 is_first = (i == 0) 216 226 is_last = (i + 1 == len(self._meta.fieldsets)) 217 output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last, 218 fieldset_start_html, fieldset_end_html, legend_tag_html)) 227 if isinstance(fieldset_or_inline, dict): 228 fields = dict((name, visible_fields[name]) for name in fieldset_or_inline['fields'] if name in visible_fields) 229 output.append(self._fieldset_html_output(fields, fieldset_or_inline, is_first, is_last, 230 fieldset_start_html, fieldset_end_html, legend_tag_html)) 231 else: 232 output.append(self._inline_html_output(fields, inlines.pop(0), is_first, is_last, 233 fieldset_start_html, fieldset_end_html, legend_tag_html)) 219 234 else: 220 235 for name in self.fields: 221 236 if name in visible_fields: … … 266 281 } 267 282 return self._html_output(**kwargs) 268 283 284 def as_tr(self): 285 "Returns this form rendered as HTML <td>s." 286 if self.has_fieldsets(): 287 raise ValueError("%s has fieldsets so its method as_tr cannot be used." % self.__class__.__name__) 288 colspan = len([bf for bf in self if not bf.is_hidden]) 289 kwargs = { 290 'row_html': u'<td%(attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>', 291 'label_tag_html': u'', 292 'help_text_html': u' %(help_text)s', 293 'top_errors_html': u'<tr><td colspan="%s">%%(top_errors)s</td></tr>\n<tr>' % colspan, 294 'fieldset_start_html': u'', 295 'fieldset_end_html': u'', 296 'legend_tag_html': u'', 297 'hidden_fields_html': u'</tr>\n<tr%%(attrs)s><td colspan="%s">%%(hidden_fields)s</td></tr>' % colspan, 298 } 299 html_output = self._html_output(**kwargs) 300 if not html_output.startswith('<tr>'): 301 html_output = u'<tr>\n%s' % html_output 302 if not html_output.endswith('</tr>'): 303 html_output = u'%s\n</tr>' % html_output 304 return html_output 305 306 def is_valid(self): 307 """ 308 Returns True if the form and its inlines have no errors. 309 """ 310 if self._is_valid is None: 311 self.full_clean() 312 return self._is_valid 313 314 def _get_errors(self): 315 "Returns an ErrorDict for the data provided for the form." 316 if self._is_valid is None: 317 self.full_clean() 318 return self._errors 319 errors = property(_get_errors) 320 269 321 def non_field_errors(self): 270 322 """ 271 323 Returns an ErrorList of errors that aren't associated with a particular … … 276 328 277 329 def full_clean(self): 278 330 """ 279 Cleans all of self.data and populates self._ errors and331 Cleans all of self.data and populates self._is_valid, self._errors and 280 332 self.cleaned_data. 281 333 """ 334 self._is_valid = True # Assume the form is valid until proven otherwise. 282 335 self._errors = ErrorDict() 283 336 if not self.is_bound: # Stop further processing. 284 337 return 285 338 self.cleaned_data = {} 339 # Process fields. 286 340 if self._meta.validation_order: 287 341 items = [(name, self.fields[name]) for name in self._meta.validation_order] 288 342 else: 289 343 items = self.fields.items() 290 for name, field in items: 291 # value_from_datadict() gets the data from the data dictionaries. 292 # Each widget type knows how to retrieve its own data, because some 293 # widgets split data over several HTML fields. 294 value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) 344 for bf in self: 295 345 try: 296 if isinstance( field, FileField):297 initial = self.initial.get( name,field.initial)298 value = field.clean(value, initial)346 if isinstance(bf.field, FileField): 347 initial = self.initial.get(bf.name, bf.field.initial) 348 value = bf.field.clean(bf.data, initial) 299 349 else: 300 value = field.clean(value) 301 self.cleaned_data[name] = value 302 if hasattr(self, 'clean_%s' % name): 303 value = getattr(self, 'clean_%s' % name)() 304 self.cleaned_data[name] = value 350 value = bf.field.clean(bf.data) 351 self.cleaned_data[bf.name] = value 352 if hasattr(self, 'clean_%s' % bf.name): 353 self.cleaned_data[bf.name] = getattr(self, 'clean_%s' % bf.name)() 305 354 except ValidationError, e: 306 self._errors[name] = self._meta.error_class(e.messages) 307 if name in self.cleaned_data: 308 del self.cleaned_data[name] 355 self._errors[bf.name] = self._meta.error_class(e.messages) 356 self._is_valid = False 357 if bf.name in self.cleaned_data: 358 del self.cleaned_data[bf.name] 359 # Process inlines. 360 for inline in self.inlines: 361 inline.full_clean() 362 if not inline.is_valid(): 363 self._is_valid = False 364 # Give self.clean() a chance to do validation. 309 365 try: 310 366 self.cleaned_data = self.clean() 311 367 except ValidationError, e: 312 368 self._errors[NON_FIELD_ERRORS] = self._meta.error_class(e.messages) 313 if self._errors: 369 self._is_valid = False 370 # If there were errors, remove the cleaned_data attribute. 371 if not self._is_valid: 314 372 delattr(self, 'cleaned_data') 315 373 316 374 def clean(self): … … 322 380 """ 323 381 return self.cleaned_data 324 382 383 def is_empty(self, fields=None, exclude=None): 384 """ 385 Returns True if the form (including inlines) is empty. Otherwise, False. 386 """ 387 for bf in self: 388 if fields and bf.name not in fields: 389 continue 390 if exclude and bf.name in exclude: 391 continue 392 if not bf.widget.is_empty(bf.data): 393 return False 394 for inline in self.inlines: 395 if not inline.is_empty(): 396 return False 397 return True 398 399 def reset(self): 400 """ 401 Resets the form (including inlines) to the state it was in 402 before data was passed to it. 403 """ 404 self.is_bound = False 405 self.data = {} 406 self.files = {} 407 self._is_valid = None 408 for inline in self.inlines: 409 inline.reset() 410 325 411 def is_multipart(self): 326 412 """ 327 Returns True if the form needs to be multipart-encrypted, i.e. it has328 FileInput. Otherwise, False.413 Returns True if the form (including inlines) needs to be 414 multipart-encrypted, i.e. it has FileInput. Otherwise, False. 329 415 """ 330 416 for field in self.fields.values(): 331 417 if field.widget.needs_multipart_form: 332 418 return True 419 for inline in self.inlines: 420 if inline.is_multipart(): 421 return True 333 422 return False 334 423 335 424 class Form(BaseForm): -
django/newforms/metaclassing.py
=== modified file 'django/newforms/metaclassing.py'
50 50 raise ImproperlyConfigured("%s cannot have more than one option from fieldsets, fields and exclude." % cls.__name__) 51 51 if cls._meta.fieldsets: 52 52 names = [] 53 for fieldset in cls._meta.fieldsets: 54 names.extend(fieldset['fields']) 53 for fieldset_or_inline in cls._meta.fieldsets: 54 if isinstance(fieldset_or_inline, dict): 55 names.extend(fieldset_or_inline['fields']) 55 56 elif cls._meta.fields: 56 57 names = cls._meta.fields 57 58 elif cls._meta.exclude: … … 59 60 else: 60 61 names = cls._base_fields_pool.keys() 61 62 cls.base_fields = SortedDict([(name, cls._base_fields_pool[name]) for name in names]) 63 64 def create_fieldsets_if_inlines_exist(cls): 65 if cls._meta.inlines is not None: 66 if cls._meta.fieldsets is not None: 67 raise ImproperlyConfigured("%s cannot have more than one option from fieldsets and inlines." % cls.__name__) 68 cls._meta.fieldsets = [{'fields': cls.base_fields.keys()}] + list(cls._meta.inlines) 69 70 def create_form_if_not_exists(cls): 71 if not cls._meta.form: 72 form_attrs = { 73 'Meta': type('Meta', (), cls._meta.__dict__), 74 } 75 for name, attr in cls.__dict__.items(): 76 if isinstance(attr, Field): 77 form_attrs[name] = attr 78 delattr(cls, name) 79 cls._meta.form = type('%sForm' % cls.__name__, (cls._meta.base_form,), form_attrs) 80 81 def check_no_fieldsets_in_form(cls): 82 if cls._meta.form._meta.fieldsets: 83 raise ImproperlyConfigured("%s cannot have form with fieldsets." % cls.__name__) 84 85 def add_fk_attribute_and_remove_fk_from_base_fields(cls): 86 # Get some options - if models are not set, this class would not be used directly. 87 parent_model, model, fk_name = cls._meta.parent_model, cls._meta.model, cls._meta.fk_name 88 if not (parent_model and model): 89 return 90 # Try to discover what the foreign key from model to parent_model is. 91 fks_to_parent = [] 92 for field in model._meta.fields: 93 # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy. 94 try: 95 if field.rel.to == parent_model: 96 fks_to_parent.append(field) 97 except AttributeError: 98 pass 99 if len(fks_to_parent) == 0: 100 raise ImproperlyConfigured("%s has no ForeignKey to %s." % (model, parent_model)) 101 if fk_name: 102 fks_to_parent = [fk for fk in fks_to_parent if fk.name == fk_name] 103 if len(fks_to_parent) > 1: 104 raise ImproperlyConfigured("%s has more than one ForeignKey to %s." % (model, parent_model)) 105 cls.fk = fks_to_parent[0] 106 # Try to remove the foreign key from base_fields to keep it transparent to the form. 107 try: 108 del cls._meta.form.base_fields[cls.fk.name] 109 except KeyError: 110 pass -
django/newforms/models.py
=== modified file 'django/newforms/models.py'
11 11 12 12 from util import ValidationError 13 13 from forms import FormOptions, FormMetaclass, BaseForm 14 from fields import Field, ChoiceField, EMPTY_VALUES 14 from formsets import FormSetOptions, FormSetMetaclass, BaseFormSet, DELETION_FIELD_NAME 15 from fields import Field, ChoiceField, EMPTY_VALUES, IntegerField, HiddenInput 15 16 from widgets import Select, SelectMultiple, MultipleHiddenInput 16 17 import metaclassing 17 18 18 19 __all__ = ( 19 20 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 20 21 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 21 'ModelChoiceField', 'ModelMultipleChoiceField' 22 'ModelChoiceField', 'ModelMultipleChoiceField', 'BaseModelFormSet', 23 'ModelFormSet', 'BaseInlineFormSet', 'InlineFormSet', 22 24 ) 23 25 24 26 def save_instance(form, instance, fields=None, fail_message='saved', … … 218 220 metaclassing.create_declared_fields(new_class) 219 221 metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class) 220 222 metaclassing.create_base_fields_from_base_fields_pool(new_class) 223 metaclassing.create_fieldsets_if_inlines_exist(new_class) 221 224 return new_class 222 225 223 226 class BaseModelForm(BaseForm): 224 227 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, instance=None): 225 opts = self._meta226 228 if instance is None: 227 # if we didn't get an instance, instantiate a new one228 self.instance = opts.model()229 # If we didn't get an instance, instantiate a new one. 230 self.instance = self._meta.model() 229 231 object_data = {} 230 232 else: 231 233 self.instance = instance 232 234 object_data = model_to_dict(instance, self.base_fields.keys()) 233 # if initial was provided, it should override the values from instance235 # If initial was provided, it should override the values from instance. 234 236 if initial is not None: 235 237 object_data.update(initial) 236 BaseForm.__init__(self, data, files, auto_id, prefix, object_data) 238 # Create inlines. 239 inlines = [] 240 if self.has_fieldsets(): 241 for fieldset_or_inline in self._meta.fieldsets: 242 if not isinstance(fieldset_or_inline, dict): 243 inline_prefix = '%s-%s' % (prefix or 'inline', len(inlines)) 244 inlines.append(fieldset_or_inline(data, files, auto_id, inline_prefix, self.instance)) 245 BaseForm.__init__(self, data, files, auto_id, prefix, object_data, inlines) 237 246 238 247 def save(self, commit=True): 239 248 """ … … 247 256 fail_message = 'created' 248 257 else: 249 258 fail_message = 'changed' 250 return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit) 259 self.saved_instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit) 260 self.saved_inline_instances = [inline.save(commit) for inline in self.inlines] 261 return self.saved_instance 251 262 252 263 class ModelForm(BaseModelForm): 253 264 __metaclass__ = ModelFormMetaclass 254 265 _options = ModelFormOptions 255 266 256 257 267 # Fields ##################################################################### 258 268 259 269 class QuerySetIterator(object): … … 364 374 else: 365 375 final_values.append(obj) 366 376 return final_values 377 378 # Model-FormSet integration ################################################### 379 380 class ModelFormSetOptions(FormSetOptions, ModelFormOptions): 381 def __init__(self, options=None): 382 super(ModelFormSetOptions, self).__init__(options) 383 # other default options 384 self.base_form = getattr(options, 'base_form', ModelForm) 385 386 class BaseModelFormSet(BaseFormSet): 387 """ 388 A ``FormSet`` for editing a queryset and/or adding new objects to it. 389 """ 390 391 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None, **kwargs): 392 if queryset is None: 393 self.queryset = self.get_queryset(**kwargs) 394 else: 395 self.queryset = queryset 396 self._pk_name = self._meta.model._meta.pk.name 397 self._pks = [obj.pk for obj in self.queryset] 398 initial = [model_to_dict(obj, self._meta.form.base_fields.keys()) for obj in self.queryset] 399 super(BaseModelFormSet, self).__init__(data, files, auto_id, prefix, initial) 400 401 def get_queryset(self, **kwargs): 402 """ 403 Hook to returning a queryset for this model. 404 """ 405 return self._meta.form._meta.model._default_manager.all() 406 407 def add_fields(self, form, index): 408 """Add a hidden field for the object's primary key.""" 409 pk = index < len(self._pks) and self._pks[index] or None 410 form.fields[self._pk_name] = IntegerField(required=False, widget=HiddenInput, initial=pk) 411 super(BaseModelFormSet, self).add_fields(form, index) 412 413 def save_new(self, form, commit=True): 414 """Saves and returns a new model instance for the given form.""" 415 return save_instance(form, self._meta.form._meta.model(), commit=commit) 416 417 def save_instance(self, form, instance, commit=True): 418 """Saves and returns an existing model instance for the given form.""" 419 return save_instance(form, instance, commit=commit) 420 421 def save(self, commit=True): 422 """Saves model instances for every form, adding and changing instances 423 as necessary, and returns the list of instances. 424 """ 425 return self.save_existing_objects(commit) + self.save_new_objects(commit) 426 427 def save_existing_objects(self, commit=True): 428 if not self.queryset: 429 return [] 430 # Put the objects from self.queryset into a dict so they are easy to lookup by pk. 431 existing_objects = {} 432 for obj in self.queryset: 433 existing_objects[obj.pk] = obj 434 # If commit is not permitted, objects cannot be deleted so they are saved as an instance variable. 435 if not commit: 436 self.instances_to_delete = [] 437 saved_instances = [] 438 for form in self.change_forms: 439 obj = existing_objects[form.cleaned_data[self._pk_name]] 440 if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 441 if commit: 442 obj.delete() 443 else: 444 self.instances_to_delete.append(obj) 445 else: 446 saved_instances.append(self.save_instance(form, obj, commit=commit)) 447 return saved_instances 448 449 def save_new_objects(self, commit=True): 450 new_objects = [] 451 for form in self.add_forms: 452 if form.is_empty(): 453 continue 454 # If someone has marked an add form for deletion, don't save the object. 455 if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 456 continue 457 new_objects.append(self.save_new(form, commit=commit)) 458 return new_objects 459 460 class ModelFormSet(BaseModelFormSet): 461 _options = ModelFormSetOptions 462 463 class InlineFormSetOptions(ModelFormSetOptions): 464 def __init__(self, options=None): 465 super(InlineFormSetOptions, self).__init__(options) 466 # new options 467 self.parent_model = getattr(options, 'parent_model', None) 468 self.fk_name = getattr(options, 'fk_name', None) 469 # other default options 470 self.deletable = getattr(options, 'deletable', True) 471 self.num_extra = getattr(options, 'num_extra', 3) 472 473 class InlineFormSetMetaclass(FormSetMetaclass): 474 def __new__(cls, name, bases, attrs): 475 new_class = type.__new__(cls, name, bases, attrs) 476 metaclassing.create_meta(new_class) 477 metaclassing.create_form_if_not_exists(new_class) 478 metaclassing.check_no_fieldsets_in_form(new_class) 479 metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class) 480 return new_class 481 482 class BaseInlineFormSet(BaseModelFormSet): 483 """A formset for child objects related to a parent.""" 484 485 def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, instance=None, **kwargs): 486 self.instance = instance 487 super(BaseInlineFormSet, self).__init__(data, files, auto_id, prefix, **kwargs) 488 489 def get_queryset(self, **kwargs): 490 """ 491 Returns this FormSet's queryset, but restricted to children of 492 self.instance 493 """ 494 if self.instance is None: 495 return self._meta.form._meta.model._default_manager.none() 496 queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 497 return queryset.filter(**{self.fk.name: self.instance}) 498 499 def save_new(self, form, commit=True): 500 kwargs = {self.fk.name: self.instance} 501 new_obj = self._meta.form._meta.model(**kwargs) 502 return save_instance(form, new_obj, commit=commit) 503 504 class InlineFormSet(BaseInlineFormSet): 505 __metaclass__ = InlineFormSetMetaclass 506 _options = InlineFormSetOptions -
django/newforms/widgets.py
=== modified file 'django/newforms/widgets.py'
69 69 """ 70 70 return data.get(name, None) 71 71 72 def is_empty(self, value): 73 """ 74 Returns True if this form is empty. 75 """ 76 if value not in (None, ''): 77 return False 78 return True 79 72 80 def id_for_label(self, id_): 73 81 """ 74 82 Returns the HTML ID attribute of this Widget for use by a <label>, … … 206 214 return False 207 215 return super(CheckboxInput, self).value_from_datadict(data, files, name) 208 216 217 def is_empty(self, value): 218 # This widget will always either be True or False, so always return the 219 # opposite value so False values will make the form empty. 220 return not value 221 209 222 class Select(Widget): 210 223 def __init__(self, attrs=None, row_attrs=None, choices=()): 211 224 super(Select, self).__init__(attrs, row_attrs) … … 248 261 value = data.get(name, None) 249 262 return {u'2': True, u'3': False, True: True, False: False}.get(value, None) 250 263 264 def is_empty(self, value): 265 # This widget will always either be True or False, so always return the 266 # opposite value so False values will make the form empty. 267 return not value 268 251 269 class SelectMultiple(Select): 252 270 def render(self, name, value, attrs=None, choices=()): 253 271 if value is None: value = [] … … 439 457 def value_from_datadict(self, data, files, name): 440 458 return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)] 441 459 460 def is_empty(self, value): 461 for widget, val in zip(self.widgets, value): 462 if not widget.is_empty(val): 463 return False 464 return True 465 442 466 def format_output(self, rendered_widgets): 443 467 """ 444 468 Given a list of rendered widgets (as strings), returns a Unicode string