Reputation: 671
When selecting a foreignkey in the django admin change form I am trying to add an href that can view the record next to the plus that adds the record.
What I've tried just to get the href to render is I've copied out the admins def render into my own custom widgets file and added it to and subclassed it:
widgets.py
class RelatedFieldWidgetWrapperLink(RelatedFieldWidgetWrapper):
def render(self, name, value, *args, **kwargs):
rel_to = self.rel.to
info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
try:
related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name)
except NoReverseMatch:
info = (self.admin_site.root_path, rel_to._meta.app_label, rel_to._meta.object_name.lower())
related_url = '%s%s/%s/add/' % info
self.widget.choices = self.choices
output = [self.widget.render(name, value, *args, **kwargs)]
if self.can_add_related:
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
(related_url, name))
output.append(u'<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Add Another')))
output.append(u'<a href="%s" class="testing" id="add_id_%s" onclick="#"> ' % \
(related_url, name))
return mark_safe(u''.join(output))
and in admin.py
formfield_overrides = {models.ForeignKey:{'widget':RelatedFieldWidgetWrapperLink}}
however I get thefollowing error:
TypeError init() takes at least 4 arguments (1 given)
Has anyone run into this problem before?
Upvotes: 3
Views: 4527
Reputation: 7030
I improved @Alasdair solution a bit:
from django.contrib.admin.templatetags import admin_static
from django.core import urlresolvers
from django.utils import safestring
from django.utils.translation import ugettext_lazy as _
class LinkedSelect(widgets.Select):
def render(self, name, value, attrs=None, *args, **kwargs):
output = [super(LinkedSelect, self).render(name, value, attrs=attrs, *args, **kwargs)]
model = self.choices.field.queryset.model
try:
obj = model.objects.get(id=value)
change_url = urlresolvers.reverse('admin:%s_%s_change' % (obj._meta.app_label, obj._meta.object_name.lower()), args=(obj.pk,))
output.append(u'<a href="%s" class="change-object" id="change_id_%s"> ' % (change_url, name))
output.append(u'<img src="%s" width="10" height="10" alt="%s"/></a>' % (admin_static.static('admin/img/icon_changelink.gif'), _('Change Object')))
except (model.DoesNotExist, urlresolvers.NoReverseMatch):
pass
return safestring.mark_safe(u''.join(output))
class YourModelAdmin(admin.ModelAdmin):
formfield_overrides = {models.ForeignKey: {'widget': LinkedSelect}}
It uses the same code structure and style as RelatedFieldWidgetWrapper
. Additionally, it uses "change" icon instead of just string. It gracefully survives when foreign key points nowhere or where foreign key points to a model which does not have admin interface defined.
Upvotes: 0
Reputation: 308899
The RelatedFieldWidgetWrapper
widget, and your subclass, are not meant to be used as the widget in formfield_overrides
. The __init__
methods have different function signatures, hence the TypeError
.
If you look at the code in django.contrib.admin.options
, you can see that the RelatedFieldWidgetWrapper
widget is instantiated in the model admin's formfield_for_dbfield
method, so that it can be passed the arguments rel
, admin_site
and can_add_related
.
I think you may have to override your model admin class' formfield_for_dbfield
method, and use your custom RelatedFieldWidgetWrapperLink
widget there.
class YourModelAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
<snip>
# ForeignKey or ManyToManyFields
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
# Combine the field kwargs with any options for formfield_overrides.
# Make sure the passed in **kwargs override anything in
# formfield_overrides because **kwargs is more specific, and should
# always win.
if db_field.__class__ in self.formfield_overrides:
kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
# Get the correct formfield.
if isinstance(db_field, models.ForeignKey):
formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
elif isinstance(db_field, models.ManyToManyField):
formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
# For non-raw_id fields, wrap the widget with a wrapper that adds
# extra HTML -- the "add other" interface -- to the end of the
# rendered output. formfield can be None if it came from a
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field.name not in self.raw_id_fields:
related_modeladmin = self.admin_site._registry.get(
db_field.rel.to)
can_add_related = bool(related_modeladmin and
related_modeladmin.has_add_permission(request))
# use your custom widget
formfield.widget = RelatedFieldWidgetWrapperLink(
formfield.widget, db_field.rel, self.admin_site,
can_add_related=can_add_related)
return formfield
<snip>
You may find it cleaner to override the formfield_for_foreignkey
method than formfield_for_dbfield
.
You may be able to subclass the Select
widget, and add your link in it's render method. Your custom select widget would then be wrapped by the RelatedFieldWidgetWrapper
. However, I am not sure whether you can produce the view_url
inside the scope of the render
method.
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.forms.widgets import Select
def get_admin_change_url(obj):
ct = ContentType.objects.get_for_model(obj)
change_url_name = 'admin:%s_%s_change' % (ct.app_label, ct.model)
return reverse(change_url_name, args=(obj.id,))
class LinkedSelect(Select):
def render(self, name, value, attrs=None, *args, **kwargs):
output = super(LinkedSelect, self).render(name, value, attrs=attrs, *args, **kwargs)
model = self.choices.field.queryset.model
try:
id = int(value)
obj = model.objects.get(id=id)
view_url = get_admin_change_url(obj)
output += mark_safe(' <a href="%s" target="_blank">view</a> ' % (view_url,))
except model.DoesNotExist:
pass
return output
class YourModelAdmin(admin.ModelAdmin):
formfield_overrides = {models.ForeignKey:{'widget':LinkedSelect}}
Upvotes: 10