-
-
Save luto/6441368fbf7e96fb1a12 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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) | |
# |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Tested and working wonderfully. I got it working with fieldsets too: https://gist.github.com/andybak/80e860673485f5ffdb08