Module for showing OneToOne fields as inline in django-admin.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | '''
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)
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
This is a nice a very usefull snippet!
I've been using it not only with OneToOne relations but with ForeignKey also changing the line 172: .... #172: if isinstance(field, OneToOneField): if isinstance(field, ForeignKey): if not isinstance(field, OneToOneField): # I added an attr 'inline_foreignkey' in the model admin # telling which fields are to be inlined if not hasattr(self, 'inline_foreignkey'): return if not field.name in self.inline_foreignkey: continue ....
It works just fine, but django doesn't use the Admin/Form defined for the related model.
When I define a custom widget for a field (in the form of the related model) it appears fine in the admin page of the related model, but not in owning model admin page (the default widget is used).
Do you have any tip how to solve this?
Thank you!
#
.... #172: if isinstance(field, OneToOneField): if isinstance(field, ForeignKey): if not isinstance(field, OneToOneField): # I added an attr 'inline_foreignkey' in the model admin # telling which fields are to be inlined if not hasattr(self, 'inline_foreignkey'): continue if not field.name in self.inline_foreignkey: continue ....
#
As you originally speculated, this doesn't work with later versions of Django. The problem arises on line 907 of contrib/admin/options.py which states:
instead of in version 1.1.1:
where the extra self.inline_instances throws up an error saying
I've found the easiest way to get it working is to copy the entire change_view function from 1.2.1 into ReverseModelAdmin and then revert lines 907 and 912/913 to the state they were in 1.1.1. Then change line 81 in the snippet to "if instance.pk" rather than "if instance.id" and it should work. I know this is a filthy hack but I've run out of time on this project!
#
Has anyone modified this useful snippets in order to be less coupled with the django core? I would like to use it in django 1.2 ad well as in django 1.3?
#
I would also find this useful in Django 1.3 +, but don't have the know how to figure out which things are updates the django core since 1.1.1 and which things are intentional changes by the author. If anyone has or does update this patch to work with newer django versions (or comes up with a non-patch way to do it) and can post your code on this site, it would be much appreciated.
#
I have this working in 1.3.
As NickJones had pointed out, there is an error about a 'queryset' argument. To fix this please make the following edit:
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, queryset=None <<< ADD THIS AS A PARAMETER):
Or if you are looking at the code snippet as is. Line 79 is where the last parameter is declared, go ahead and add another one "queryset=None".
This worked for me and is fast.
Thank you everyone for the comments and updates.
#
updated with ability define custom forms:
https://gist.github.com/4343464
#
Has anyone got an equivalent snippet for this, for django 1.9?
#
Second the ask on having this snippet on django 1.9. If someone has implemented it, it would be super helpful!
#
Just ported this to 1.9.6 and started hosting the code in github. https://github.com/daniyalzade/django_reverse_admin. Feel free to use it at your will and submit PRs as you see fit.
#
Please login first before commenting.