Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Link To Foreignkey in Admin Causes AttributeError When Debug Is False

I have used the following code in my models.py file:

Create hyperlink to foreignkey

class ModelAdminWithForeignKeyLinksMetaclass(MediaDefiningClass): 

    def __getattr__(cls, name):

        def foreign_key_link(instance, field):
            target = getattr(instance, field)
            return u'<a href="../../%s/%s/%s">%s</a>' % (
                target._meta.app_label, target._meta.module_name, target.id, unicode(target))

        if name[:8] == 'link_to_':
            method = partial(foreign_key_link, field=name[8:])
            method.__name__ = name[8:]
            method.allow_tags = True
            setattr(cls, name, method)
            return getattr(cls, name)
        raise AttributeError

In admin.py list_display I've added link_to to the beginning of each field I want a foreignkey link on. This works really well however when I turn debug off I get an attribute error. Any suggestions?

like image 386
Crazyconoli Avatar asked Dec 13 '22 11:12

Crazyconoli


2 Answers

I stumbled on exactly the same problem, luckily, I've fixed it.

The original solution (the one you used) comes from this question, my solution is based on it:

class ForeignKeyLinksMetaclass(MediaDefiningClass):

    def __new__(cls, name, bases, attrs):

        new_class = super(
            ForeignKeyLinksMetaclass, cls).__new__(cls, name, bases, attrs)

        def foreign_key_link(instance, field):
            target = getattr(instance, field)
            return u'<a href="../../%s/%s/%d/">%s</a>' % (
                target._meta.app_label, target._meta.module_name,
                target.id, unicode(target)
            )

        for name in new_class.list_display:
            if name[:8] == 'link_to_':
                method = partial(foreign_key_link, field=name[8:])
                method.__name__ = name[8:]
                method.allow_tags = True
                setattr(new_class, name, method)

        return new_class

Well, the only thing you need is to replace the original ModelAdminWithForeignKeyLinksMetaclass with the one above.

However, it's not the end. The most interesting part is why the original solution causes problems. The answer to this question lies here (line 31) and here (line 244).

When DEBUG is on Django tries to validate all registered ModelAdmins (first link). There cls is a class SomeAdmin (i.e. an instance of its metaclass). When hasattr is called, python tries to find an attribute field in class SomeAdmin or in one of its super classes. Since it is impossible, __getattr__ of its class (i.e. SomeAdmin's metaclass) is called, where a new method is added to class SomeAdmin. Hence, when it comes to rendering the interface, SomeAdmin is already patched and Django is able to find the required field (second link).

When DEBUG is False, Django skips the validation. When the interface is rendered Django tries to find a field (again, second link), but this time SomeAdmin is not patched, moreover model_admin is not class SomeAdmin, it is its instance. Thus, trying to find an attribute name in model_admin, python is unable to do this, neither it is able to find it in its class (SomeAdmin) as well as in any of its super classes, so an exception is raised.

like image 88
stepank Avatar answered May 09 '23 13:05

stepank


I uses stepank's implementation, but had to alter it slightly to fit my use-case.

I basically just added support for 'ModelAdmin.readonly_fields' and 'ModelAdmin.fields' to support the 'link_to_'-syntax.

A slight change to the link creation also allowed me to support a link to a different APP.Model, in my case the built-in django.contrib.auth.models.user.

Thanks for the nice work @stepank and @Itai Tavor.

I hope this is useful for somebody else.


DEFAULT_LOGGER_NAME is defined in my settings.py and I use it for most of my logging. If you don't have it defined, you will get errors when using the following code. You can either define your own DEFAULT_LOGGER_NAME in settings.py (it is just a simple string) or you just remove all references to the logger in the code below.

'''
Created on Feb 23, 2012

@author: daniel

Purpose: Provides a 'link_to_<foreignKeyModel>' function for ModelAdmin 
         implementations. This is based on the following work:

original: http://stackoverflow.com/a/3157065/193165
fixed original: http://stackoverflow.com/a/7192721/193165
'''
from functools      import partial
from django.forms   import MediaDefiningClass

import logging
from public.settings import DEFAULT_LOGGER_NAME
logger = logging.getLogger(DEFAULT_LOGGER_NAME)

class ForeignKeyLinksMetaclass(MediaDefiningClass):

    def __new__(cls, name, bases, attrs):

        new_class = super(
            ForeignKeyLinksMetaclass, cls).__new__(cls, name, bases, attrs)

        def foreign_key_link(instance, field):
            target = getattr(instance, field)
            ret_url = u'<a href="../../%s/%s/%d/">%s</a>' % (
                      target._meta.app_label, target._meta.module_name,
                      target.id, unicode(target)
                      ) 
            #I don't know how to dynamically determine in what APP we currently
            #are, so this is a bit of a hack to enable links to the 
            #django.contrib.auth.models.user
            if "auth" in target._meta.app_label and "user" in target._meta.module_name:
                ret_url = u'<a href="/admin/%s/%s/%d/">%s</a>' % (
                          target._meta.app_label, target._meta.module_name,
                          target.id, unicode(target)
                          )                    
            return ret_url

        def _add_method(name):
            if name is None: return
            if isinstance(name, basestring) and name[:8] == 'link_to_':
                try:
                    method = partial(foreign_key_link, field=name[8:])
                    method.__name__ = name[8:]
                    method.allow_tags = True
                    #in my app the "user" field always points to django.contrib.auth.models.user
                    #and I want my users to see that when they edit "client" data
                    #"Client" is another model, that has a 1:1 relationship with 
                    #django.contrib.auth.models.user
                    if "user" in name[8:]: 
                        method.short_description = "Auth User"
                    setattr(new_class, name, method)
                except Exception, ex:
                    logger.debug("_add_method(%s) failed: %s" % (name, ex))
        #make this work for InlineModelAdmin classes as well, who do not have a
        #.list_display attribute
        if hasattr(new_class, "list_display") and not new_class.list_display is None:
            for name in new_class.list_display:
                _add_method(name)
        #enable the 'link_to_<foreignKeyModel>' syntax for the ModelAdmin.readonly_fields
        if not new_class.readonly_fields is None:
            for name in new_class.readonly_fields:
                _add_method(name)
        #enable the 'link_to_<foreignKeyModel>' syntax for the ModelAdmin.fields
        if not new_class.fields is None:
            for name in new_class.fields:
                _add_method(name)

        return new_class
like image 44
daniel Avatar answered May 09 '23 13:05

daniel