''' reverseadmin ============ Module that makes django admin handle OneToOneFields in a better way. A common use case for one-to-one relationships is to "embed" a model inside another one. For example, a Person may have multiple foreign keys pointing to an Address entity, one home address, one business address and so on. Django admin displays those relations using select boxes, letting the user choose which address entity to connect to a person. A more natural way to handle the relationship is using inlines. However, since the foreign key is placed on the owning entity, django admins standard inline classes can't be used. Which is why I created this module that implements "reverse inlines" for this use case. Example: from django.db import models class Address(models.Model): street = models.CharField(max_length = 255) zipcode = models.CharField(max_length = 10) city = models.CharField(max_length = 255) class Person(models.Model): name = models.CharField(max_length = 255) business_addr = models.OneToOneField(Address, related_name = 'business_addr') home_addr = models.OneToOneField(Address, related_name = 'home_addr') This is how standard django admin renders it: http://img9.imageshack.us/i/beforetz.png/ Here is how it looks when using the reverseadmin module: http://img408.imageshack.us/i/afterw.png/ You use reverseadmin in the following way: from django.contrib import admin from models import Person from reverseadmin import ReverseModelAdmin class PersonAdmin(ReverseModelAdmin): inline_type = 'tabular' admin.site.register(Person, PersonAdmin) inline_type can be either "tabular" or "stacked" for tabular and stacked inlines respectively. The module is designed to work with Django 1.1.1. Since it hooks into the internals of the admin package, it may not work with later Django versions. ''' from django.contrib.admin import helpers, ModelAdmin from django.contrib.admin.options import InlineModelAdmin from django.db import transaction from django.db.models import OneToOneField from django.forms import ModelForm from django.forms.formsets import all_valid from django.forms.models import BaseModelFormSet, modelformset_factory from django.utils.encoding import force_unicode from django.utils.functional import curry from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ class ReverseInlineFormSet(BaseModelFormSet): ''' A formset with either a single object or a single empty form. Since the formset is used to render a required OneToOne relation, the forms must not be empty. ''' model = None parent_fk_name = '' def __init__(self, data = None, files = None, instance = None, prefix = None, save_as_new = False): if instance.id: object = getattr(instance, self.parent_fk_name) qs = self.model.objects.filter(pk = object.id) else: qs = self.model.objects.filter(pk = -1) self.extra = 1 super(ReverseInlineFormSet, self).__init__(data, files, prefix = prefix, queryset = qs) for form in self.forms: form.empty_permitted = False def reverse_inlineformset_factory(parent_model, model, parent_fk_name, form = ModelForm, fields = None, exclude = None, formfield_callback = lambda f: f.formfield()): kwargs = { 'form': form, 'formfield_callback': formfield_callback, 'formset': ReverseInlineFormSet, 'extra': 0, 'can_delete': False, 'can_order': False, 'fields': fields, 'exclude': exclude, 'max_num': 1, } FormSet = modelformset_factory(model, **kwargs) FormSet.parent_fk_name = parent_fk_name return FormSet class ReverseInlineModelAdmin(InlineModelAdmin): ''' Use the name and the help_text of the owning models field to render the verbose_name and verbose_name_plural texts. ''' def __init__(self, parent_model, parent_fk_name, model, admin_site, inline_type): self.template = 'admin/edit_inline/%s.html' % inline_type self.parent_fk_name = parent_fk_name self.model = model field_descriptor = getattr(parent_model, self.parent_fk_name) field = field_descriptor.field self.verbose_name_plural = field.verbose_name.title() self.verbose_name = field.help_text if not self.verbose_name: self.verbose_name = self.verbose_name_plural super(ReverseInlineModelAdmin, self).__init__(parent_model, admin_site) def get_formset(self, request, obj = None, **kwargs): if self.declared_fieldsets: fields = flatten_fieldsets(self.declared_fieldsets) else: fields = None if self.exclude is None: exclude = [] else: exclude = list(self.exclude) # if exclude is an empty list we use None, since that's the actual # default exclude = (exclude + kwargs.get("exclude", [])) or None defaults = { "form": self.form, "fields": fields, "exclude": exclude, "formfield_callback": curry(self.formfield_for_dbfield, request=request), } defaults.update(kwargs) return reverse_inlineformset_factory(self.parent_model, self.model, self.parent_fk_name, **defaults) class ReverseModelAdmin(ModelAdmin): ''' Patched ModelAdmin class. The add_view method is overridden to allow the reverse inline formsets to be saved before the parent model. ''' def __init__(self, model, admin_site): super(ReverseModelAdmin, self).__init__(model, admin_site) if self.exclude is None: self.exclude = [] for field in model._meta.fields: if isinstance(field, OneToOneField): name = field.name parent = field.related.parent_model inline = ReverseInlineModelAdmin(model, name, parent, admin_site, self.inline_type) self.inline_instances.append(inline) self.exclude.append(name) def add_view(self, request, form_url='', extra_context=None): "The 'add' admin view for this model." model = self.model opts = model._meta if not self.has_add_permission(request): raise PermissionDenied ModelForm = self.get_form(request) formsets = [] if request.method == 'POST': form = ModelForm(request.POST, request.FILES) if form.is_valid(): form_validated = True new_object = self.save_form(request, form, change=False) else: form_validated = False new_object = self.model() prefixes = {} for FormSet in self.get_formsets(request): prefix = FormSet.get_default_prefix() prefixes[prefix] = prefixes.get(prefix, 0) + 1 if prefixes[prefix] != 1: prefix = "%s-%s" % (prefix, prefixes[prefix]) formset = FormSet(data=request.POST, files=request.FILES, instance=new_object, save_as_new=request.POST.has_key("_saveasnew"), prefix=prefix) formsets.append(formset) if all_valid(formsets) and form_validated: # Here is the modified code. for formset, inline in zip(formsets, self.inline_instances): if not isinstance(inline, ReverseInlineModelAdmin): continue obj = formset.save()[0] setattr(new_object, inline.parent_fk_name, obj) self.save_model(request, new_object, form, change=False) form.save_m2m() for formset in formsets: self.save_formset(request, form, formset, change=False) self.log_addition(request, new_object) return self.response_add(request, new_object) else: # Prepare the dict of initial data from the request. # We have to special-case M2Ms as a list of comma-separated PKs. initial = dict(request.GET.items()) for k in initial: try: f = opts.get_field(k) except models.FieldDoesNotExist: continue if isinstance(f, models.ManyToManyField): initial[k] = initial[k].split(",") form = ModelForm(initial=initial) prefixes = {} for FormSet in self.get_formsets(request): prefix = FormSet.get_default_prefix() prefixes[prefix] = prefixes.get(prefix, 0) + 1 if prefixes[prefix] != 1: prefix = "%s-%s" % (prefix, prefixes[prefix]) formset = FormSet(instance=self.model(), prefix=prefix) formsets.append(formset) adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) media = self.media + adminForm.media inline_admin_formsets = [] for inline, formset in zip(self.inline_instances, formsets): fieldsets = list(inline.get_fieldsets(request)) inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets) inline_admin_formsets.append(inline_admin_formset) media = media + inline_admin_formset.media context = { 'title': _('Add %s') % force_unicode(opts.verbose_name), 'adminform': adminForm, 'is_popup': request.REQUEST.has_key('_popup'), 'show_delete': False, 'media': mark_safe(media), 'inline_admin_formsets': inline_admin_formsets, 'errors': helpers.AdminErrorList(form, formsets), 'root_path': self.admin_site.root_path, 'app_label': opts.app_label, } context.update(extra_context or {}) return self.render_change_form(request, context, form_url=form_url, add=True) add_view = transaction.commit_on_success(add_view)