| 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 | |