| | 263 | |
| | 264 | from django.newforms.models import BaseModelFormSet, formset_for_model, save_instance |
| | 265 | from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets |
| | 266 | |
| | 267 | def get_foreign_key(model, ct_field_name, id_field_name): |
| | 268 | opts = model._meta |
| | 269 | # avoid circular import |
| | 270 | from django.db.models import ForeignKey |
| | 271 | from django.contrib.contenttypes.models import ContentType |
| | 272 | opts = model._meta |
| | 273 | |
| | 274 | # if there is no field called `ct_field_name` let the exception propagate |
| | 275 | ct = opts.get_field(ct_field_name) |
| | 276 | if not isinstance(ct, ForeignKey) or ct.rel.to != ContentType: |
| | 277 | raise Exception("fk_name '%s' is not a ForeignKey to ContentType" % (ct_field_name)) |
| | 278 | |
| | 279 | # if there is no field called `id_field_name` let the exception propagate |
| | 280 | obj_id = opts.get_field(id_field_name) |
| | 281 | |
| | 282 | return ct, obj_id |
| | 283 | |
| | 284 | def inline_formset(model, ct_field_name, id_field_name, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield(), formset=None): |
| | 285 | """ |
| | 286 | Returns an ``InlineFormset`` for the given kwargs. |
| | 287 | |
| | 288 | You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` |
| | 289 | to ``parent_model``. |
| | 290 | """ |
| | 291 | ct, obj_id = get_foreign_key(model, ct_field_name, id_field_name) |
| | 292 | # let the formset handle object deletion by default |
| | 293 | FormSet = formset_for_model(model, formset=formset or GenericInlineFormset, fields=fields, |
| | 294 | formfield_callback=formfield_callback, |
| | 295 | extra=extra, orderable=orderable, |
| | 296 | deletable=deletable) |
| | 297 | # HACK: remove the ForeignKey to the parent from every form |
| | 298 | # This should be done a line above before we pass 'fields' to formset_for_model |
| | 299 | # an 'omit' argument would be very handy here |
| | 300 | try: |
| | 301 | del FormSet.form_class.base_fields[ct.name] |
| | 302 | del FormSet.form_class.base_fields[obj_id.name] |
| | 303 | except KeyError: |
| | 304 | pass |
| | 305 | FormSet.ct = ct |
| | 306 | FormSet.obj_id = obj_id |
| | 307 | return FormSet |
| | 308 | |
| | 309 | class GenericInlineFormset(BaseModelFormSet): |
| | 310 | """A formset for child objects related to a parent.""" |
| | 311 | def __init__(self, instance=None, data=None, files=None): |
| | 312 | self.instance = instance |
| | 313 | # is there a better way to get the object descriptor? |
| | 314 | self.rel_name = self.ct.name + '_' + self.obj_id.name |
| | 315 | super(GenericInlineFormset, self).__init__(data, files, prefix=self.rel_name) |
| | 316 | |
| | 317 | def get_queryset(self): |
| | 318 | # This import is done here to avoid circular import importing this module |
| | 319 | from django.contrib.contenttypes.models import ContentType |
| | 320 | if self.instance is None: |
| | 321 | return [] |
| | 322 | return self.model._default_manager.filter( **{ |
| | 323 | self.ct.name : ContentType.objects.get_for_model(self.instance), |
| | 324 | self.obj_id.name : self.instance._get_pk_val() |
| | 325 | }) |
| | 326 | |
| | 327 | def save_new(self, form, commit=True): |
| | 328 | # This import is done here to avoid circular import importing this module |
| | 329 | from django.contrib.contenttypes.models import ContentType |
| | 330 | kwargs = { |
| | 331 | self.ct.get_attname(): ContentType.objects.get_for_model(self.instance).id, |
| | 332 | self.obj_id.get_attname(): self.instance._get_pk_val(), |
| | 333 | } |
| | 334 | new_obj = self.model(**kwargs) |
| | 335 | return save_instance(form, new_obj, commit=commit) |
| | 336 | |
| | 337 | class GenericInlineModelAdmin(InlineModelAdmin): |
| | 338 | ct_field_name = None |
| | 339 | id_field_name = None |
| | 340 | def formset_add(self, request): |
| | 341 | """Returns an GenericInlineFormSet class for use in admin add views.""" |
| | 342 | if self.declared_fieldsets: |
| | 343 | fields = flatten_fieldsets(self.declared_fieldsets) |
| | 344 | else: |
| | 345 | fields = None |
| | 346 | return inline_formset(self.model, self.ct_field_name, self.id_field_name, fields=fields, |
| | 347 | formfield_callback=self.formfield_for_dbfield, extra=self.extra, formset=self.formset) |
| | 348 | |
| | 349 | def formset_change(self, request, obj): |
| | 350 | """Returns an GenericInlineFormSet class for use in admin change views.""" |
| | 351 | if self.declared_fieldsets: |
| | 352 | fields = flatten_fieldsets(self.declared_fieldsets) |
| | 353 | else: |
| | 354 | fields = None |
| | 355 | return inline_formset(self.model, self.ct_field_name, self.id_field_name, fields=fields, |
| | 356 | formfield_callback=self.formfield_for_dbfield, extra=self.extra, formset=self.formset) |
| | 357 | |
| | 358 | class GenericStackedInline(GenericInlineModelAdmin): |
| | 359 | template = 'admin/edit_inline/stacked.html' |
| | 360 | |
| | 361 | class GenericTabularInline(GenericInlineModelAdmin): |
| | 362 | template = 'admin/edit_inline/tabular.html' |
| | 363 | |