Skip to content

Instantly share code, notes, and snippets.

@luto
Forked from andybak/reverseadmin.py
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save luto/6441368fbf7e96fb1a12 to your computer and use it in GitHub Desktop.
Save luto/6441368fbf7e96fb1a12 to your computer and use it in GitHub Desktop.
"""
adminreverse from here http://djangosnippets.org/snippets/2032/
changed for working with ForeignKeys
Fixed for Django 1.6 by @andybak
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.ForeignKey(Address,
related_name = 'business_addr')
home_addr = models.OneToOneField(Address, related_name = 'home_addr')
other_addr = models.OneToOneField(Address, related_name = 'other_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 django.db import models
from models import Person
from reverseadmin import ReverseModelAdmin
class AddressForm(models.Form):
pass
class PersonAdmin(ReverseModelAdmin):
inline_type = 'tabular'
inline_reverse = ('business_addr', ('home_addr', AddressForm), ('other_addr' (
'form': OtherForm
'exclude': ()
)))
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.6. Since it hooks into
the internals of the admin package, it may not work with later Django
versions.
"""
# TODO
# Never got this working but might come back to it
# class PurchaseOrderAdmin(ReverseModelAdmin):
#
# inline_type = 'stacked'
# inline_reverse = (
# ('arrival', {
# 'fields': (
# ('start_location', 'time_at_start'),
# ('end_location', 'time_at_end'),
# ('transit_company', 'transit_person'),
# ),
# },),
# )
from django.contrib.admin import helpers, ModelAdmin
from django.contrib.admin.options import InlineModelAdmin, csrf_protect_m, IS_POPUP_VAR
from django.contrib.admin.util import flatten_fieldsets, unquote
from django.core.urlresolvers import reverse
from django.db import transaction, models
from django.db.models import OneToOneField, ForeignKey
from django.forms import ModelForm
from django.forms.formsets import all_valid
from django.forms.models import BaseModelFormSet, modelformset_factory
from django.http import Http404
from django.utils.encoding import force_unicode, force_text
from django.utils.functional import curry
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from django.core.exceptions import PermissionDenied
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,
queryset=None,
save_as_new=False):
object = getattr(instance, self.parent_fk_name)
if object:
qs = self.model.objects.filter(pk = object.id)
self.extra = 0
self.max_num = 0
else:
qs = self.model.objects.filter(pk = -1)
self.extra = 0
self.max_num = 0
super(ReverseInlineFormSet, self).__init__(
data,
files,
prefix=prefix,
queryset=qs
)
for form in self.forms:
form.empty_permitted = False
def reverse_inlineformset_factory(
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': 0,
}
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 has_add_permission(self, request):
return False
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.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)
# Initialize reverse_inline_instances
if self.exclude is None:
self.exclude = []
self.reverse_inline_instances = []
for field_config in self.inline_reverse:
kwargs = {}
if isinstance(field_config, tuple):
field_name = field_config[0]
if isinstance(field_config[1], dict):
kwargs = field_config[1]
elif isinstance(field_config[1], ModelForm):
kwargs['form'] = field_config[1]
else:
field_name = field_config
field = model._meta.get_field(field_name)
if isinstance(field, (OneToOneField, ForeignKey)):
name = field.name
parent = field.related.parent_model
inline = ReverseInlineModelAdmin(
model,
name,
parent,
admin_site,
self.inline_type
)
if kwargs:
inline.__dict__.update(kwargs)
self.reverse_inline_instances.append(inline)
self.exclude.append(name)
def get_inline_instances(self, request, obj=None):
inline_instances = super(ReverseModelAdmin, self).get_inline_instances(request, obj)
return self.reverse_inline_instances + inline_instances
@csrf_protect_m
@transaction.atomic
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 = []
inline_instances = self.get_inline_instances(request, None)
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES)
if form.is_valid():
new_object = self.save_form(request, form, change=False)
form_validated = True
else:
form_validated = False
new_object = self.model()
prefixes = {}
for FormSet, inline in zip(self.get_formsets(request), inline_instances):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(data=request.POST, files=request.FILES,
instance=new_object,
save_as_new="_saveasnew" in request.POST,
prefix=prefix, queryset=inline.get_queryset(request))
formsets.append(formset)
if all_valid(formsets) and form_validated:
# Here is the modified code.
for formset, inline in zip(formsets, self.get_inline_instances(request)):
if not isinstance(inline, ReverseInlineModelAdmin):
continue
objects = formset.save()
if len(objects)==1:
setattr(new_object, inline.parent_fk_name, objects[0])
# End modified code
self.save_model(request, new_object, form, False)
self.save_related(request, form, formsets, 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, inline in zip(self.get_formsets(request), inline_instances):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(instance=self.model(), prefix=prefix,
queryset=inline.get_queryset(request))
formsets.append(formset)
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
self.get_prepopulated_fields(request),
self.get_readonly_fields(request),
model_admin=self)
media = self.media + adminForm.media
inline_admin_formsets = []
for inline, formset in zip(inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request))
readonly = list(inline.get_readonly_fields(request))
prepopulated = dict(inline.get_prepopulated_fields(request))
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
fieldsets, prepopulated, readonly, model_admin=self)
inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media
context = {
'title': _('Add %s') % force_text(opts.verbose_name),
'adminform': adminForm,
'is_popup': IS_POPUP_VAR in request.REQUEST,
'media': media,
'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label,
'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return self.render_change_form(request, context, form_url=form_url, add=True)
# TODO Not sure we need this
#
# @csrf_protect_m
# @transaction.atomic
# def change_view(self, request, object_id, form_url='', extra_context=None):
# "The 'change' admin view for this model."
# model = self.model
# opts = model._meta
#
# obj = self.get_object(request, unquote(object_id))
#
# if not self.has_change_permission(request, obj):
# raise PermissionDenied
#
# if obj is None:
# raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)})
#
# if request.method == 'POST' and "_saveasnew" in request.POST:
# return self.add_view(request, form_url=reverse('admin:%s_%s_add' %
# (opts.app_label, opts.model_name),
# current_app=self.admin_site.name))
#
# ModelForm = self.get_form(request, obj)
# formsets = []
# inline_instances = self.get_inline_instances(request, obj)
# if request.method == 'POST':
# form = ModelForm(request.POST, request.FILES, instance=obj)
# if form.is_valid():
# form_validated = True
# new_object = self.save_form(request, form, change=True)
# else:
# form_validated = False
# new_object = obj
# prefixes = {}
# for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
# prefix = FormSet.get_default_prefix()
# prefixes[prefix] = prefixes.get(prefix, 0) + 1
# if prefixes[prefix] != 1 or not prefix:
# prefix = "%s-%s" % (prefix, prefixes[prefix])
# formset = FormSet(request.POST, request.FILES,
# instance=new_object, prefix=prefix,
# queryset=inline.get_queryset(request))
#
# formsets.append(formset)
#
# if all_valid(formsets) and form_validated:
#
# # Here is the modified code.
#
# for formset, inline in zip(formsets, self.get_inline_instances(request)):
# if not isinstance(inline, ReverseInlineModelAdmin):
# continue
# objects = formset.save()
# if len(objects)==1:
# setattr(new_object, inline.parent_fk_name, objects[0])
# # End modified code
#
#
# self.save_model(request, new_object, form, True)
# self.save_related(request, form, formsets, True)
# change_message = self.construct_change_message(request, form, formsets)
# self.log_change(request, new_object, change_message)
# return self.response_change(request, new_object)
#
# else:
# form = ModelForm(instance=obj)
# prefixes = {}
# for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
# prefix = FormSet.get_default_prefix()
# prefixes[prefix] = prefixes.get(prefix, 0) + 1
# if prefixes[prefix] != 1 or not prefix:
# prefix = "%s-%s" % (prefix, prefixes[prefix])
# formset = FormSet(instance=obj, prefix=prefix,
# queryset=inline.get_queryset(request))
# formsets.append(formset)
#
# adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
# self.get_prepopulated_fields(request, obj),
# self.get_readonly_fields(request, obj),
# model_admin=self)
# media = self.media + adminForm.media
#
# inline_admin_formsets = []
# for inline, formset in zip(inline_instances, formsets):
# fieldsets = list(inline.get_fieldsets(request, obj))
# readonly = list(inline.get_readonly_fields(request, obj))
# prepopulated = dict(inline.get_prepopulated_fields(request, obj))
# inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
# fieldsets, prepopulated, readonly, model_admin=self)
# inline_admin_formsets.append(inline_admin_formset)
# media = media + inline_admin_formset.media
#
# context = {
# 'title': _('Change %s') % force_text(opts.verbose_name),
# 'adminform': adminForm,
# 'object_id': object_id,
# 'original': obj,
# 'is_popup': IS_POPUP_VAR in request.REQUEST,
# 'media': media,
# 'inline_admin_formsets': inline_admin_formsets,
# 'errors': helpers.AdminErrorList(form, formsets),
# 'app_label': opts.app_label,
# 'preserved_filters': self.get_preserved_filters(request),
# }
# context.update(extra_context or {})
# return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
#
@andybak
Copy link

andybak commented Aug 2, 2014

Tested and working wonderfully. I got it working with fieldsets too: https://gist.github.com/andybak/80e860673485f5ffdb08

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment