|
|
@@ -0,0 +1,479 @@ |
|
|
""" |
|
|
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): |
|
|
if isinstance(field_config[1], dict): |
|
|
kwargs = field_config[1] |
|
|
elif isinstance(field_config[1], ModelForm): |
|
|
kwargs['form'] = field_config[1] |
|
|
|
|
|
field_name = field_config[0] |
|
|
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) |
|
|
# |