[Django]-Customize Django Admin Change Form Foreignkey to Include View Record

9đź‘Ť

âś…

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>

Other approaches

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('&nbsp;<a href="%s" target="_blank">view</a>&nbsp;' % (view_url,))
        except model.DoesNotExist:
            pass
        return output

class YourModelAdmin(admin.ModelAdmin):
    formfield_overrides = {models.ForeignKey:{'widget':LinkedSelect}}
👤Alasdair

0đź‘Ť

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.

👤Mitar

Leave a comment