【问题标题】:How do I add a link from the Django admin page of one object to the admin page of a related object?如何将一个对象的 Django 管理页面的链接添加到相关对象的管理页面?
【发布时间】:2012-04-12 18:28:06
【问题描述】:

为了处理 django-admin 中的 lack of nested inlines,我将特殊情况放入了两个模板中,以在两个模型的管理员更改页面和内联管理员之间创建链接。

我的问题是:如何干净利落地创建从一个模型的管理员更改页面或内联管理员到相关模型的管理员更改页面或内联管理员的链接,而不会在模板中出现讨厌的黑客攻击?强>

我想要一个通用的解决方案,我可以将其应用于任何模型的管理员更改页面或内联管理员。


我有一个模型,post(不是它的真名),它既是 blog 管理页面上的内联模型,也有自己的管理页面。它不能只是内联的原因是它具有带有外键的模型,只有在使用它编辑时才有意义,并且只有在使用 blog 编辑时才有意义。

对于post 管理页面,我将“fieldset.html”的一部分从:

{% if field.is_readonly %}
    <p>{{ field.contents }}</p>
{% else %}
    {{ field.field }}
{% endif %}

{% if field.is_readonly %}
    <p>{{ field.contents }}</p>
{% else %}
    {% ifequal field.field.name "blog" %}
        <p>{{ field.field.form.instance.blog_link|safe }}</p>
    {% else %}
        {{ field.field }}
    {% endifequal %}
{% endif %}

创建指向blog 管理页面的链接,其中blog_link 是模型上的一个方法:

def blog_link(self):
      return '<a href="%s">%s</a>' % (reverse("admin:myblog_blog_change",  
                                        args=(self.blog.id,)), escape(self.blog))

我在field.field.form.instance 之外的任何地方都找不到blog 实例的id

blog 管理页面上,post 是内联的,我从以下位置修改了“stacked.html”的一部分:

<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;
<span class="inline_label">{% if inline_admin_form.original %}
    {{ inline_admin_form.original }}
{% else %}#{{ forloop.counter }}{% endif %}</span>

<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;
<span class="inline_label">{% if inline_admin_form.original %}
    {% ifequal inline_admin_formset.opts.verbose_name "post" %}
    <a href="/admin/myblog/post/{{ inline_admin_form.pk_field.field.value }}/">
            {{ inline_admin_form.original }}</a>
{% else %}{{ inline_admin_form.original }}{% endifequal %}
{% else %}#{{ forloop.counter }}{% endif %}</span>

创建指向post 管理页面的链接,因为在这里我能够找到存储在外键字段中的id


我确信有一种更好、更通用的方法可以在不重复自己的情况下添加管理表单的链接;这是什么?

【问题讨论】:

  • 试图解决同样的问题,以下解决方案在 python 3/django 1.6 上对我没有帮助。不过This post确实很好地解决了问题,我想分享一下。
  • @rara_tiru 好链接!谢谢。这解决了几乎相同的问题,希望我在问这个问题之前找到它。

标签: python django django-models django-admin django-templates


【解决方案1】:

Django 1.8 中的新功能:show_change_link for inline admin

在内联模型中将 show_change_link 设置为 True(默认为 False),以便内联对象具有指向其更改表单的链接(它们可以有自己的内联)。

from django.contrib import admin

class PostInline(admin.StackedInline):
    model = Post
    show_change_link = True
    ...

class BlogAdmin(admin.ModelAdmin):
    inlines = [PostInline]
    ...

class ImageInline(admin.StackedInline):
    # Assume Image model has foreign key to Post
    model = Image
    show_change_link = True
    ...

class PostAdmin(admin.ModelAdmin):
    inlines = [ImageInline]
    ...

admin.site.register(Blog, BlogAdmin)
admin.site.register(Post, PostAdmin)

【讨论】:

  • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。
  • 感谢您发布此信息!很高兴终于添加了这个功能。
【解决方案2】:

根据 agfs 和 SummerBreeze 的建议,我改进了装饰器以更好地处理 unicode 并能够链接到反向外键字段(ManyRelatedManager 有一个结果)。您现在还可以添加一个 short_description 作为列表标题:

from django.core.urlresolvers import reverse
from django.core.exceptions import MultipleObjectsReturned
from django.utils.safestring import mark_safe

def add_link_field(target_model=None, field='', app='', field_name='link',
                   link_text=unicode, short_description=None):
    """
    decorator that automatically links to a model instance in the admin;
    inspired by http://stackoverflow.com/questions/9919780/how-do-i-add-a-link-from-the-django-admin-page-of-one-object-
    to-the-admin-page-o
    :param target_model: modelname.lower or model
    :param field: fieldname
    :param app: appname
    :param field_name: resulting field name
    :param link_text: callback to link text function
    :param short_description: list header
    :return:
    """
    def add_link(cls):
        reverse_name = target_model or cls.model.__name__.lower()

        def link(self, instance):
            app_name = app or instance._meta.app_label
            reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
            link_obj = getattr(instance, field, None) or instance

            # manyrelatedmanager with one result?
            if link_obj.__class__.__name__ == "RelatedManager":
                try:
                    link_obj = link_obj.get()
                except MultipleObjectsReturned:
                    return u"multiple, can't link"
                except link_obj.model.DoesNotExist:
                    return u""

            url = reverse(reverse_path, args = (link_obj.id,))
            return mark_safe(u"<a href='%s'>%s</a>" % (url, link_text(link_obj)))
        link.allow_tags = True
        link.short_description = short_description or (reverse_name + ' link')
        setattr(cls, field_name, link)
        cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
            [field_name]
        return cls
    return add_link

编辑:由于链接消失而更新。

【讨论】:

  • 您的链接已失效。一般来说,请在答案本身中包含代码,以免发生这种情况。
  • 谢谢,已编辑。我以为这么大的粘贴在这里会出错。
【解决方案3】:

我认为 agf 的解决方案非常棒 - 对他赞不绝口。但我需要更多功能:

  • 一个管理员可以拥有多个链接
  • 能够链接到不同应用中的模型

解决方案:

def add_link_field(target_model = None, field = '', app='', field_name='link',
                   link_text=unicode):
    def add_link(cls):
        reverse_name = target_model or cls.model.__name__.lower()
        def link(self, instance):
            app_name = app or instance._meta.app_label
            reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
            link_obj = getattr(instance, field, None) or instance
            url = reverse(reverse_path, args = (link_obj.id,))
            return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
        link.allow_tags = True
        link.short_description = reverse_name + ' link'
        setattr(cls, field_name, link)
        cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
            [field_name]
        return cls
    return add_link

用法:

# 'apple' is name of model to link to
# 'fruit_food' is field name in `instance`, so instance.fruit_food = Apple()
# 'link2' will be name of this field
@add_link_field('apple','fruit_food',field_name='link2')
# 'cheese' is name of model to link to
# 'milk_food' is field name in `instance`, so instance.milk_food = Cheese()
# 'milk' is the name of the app where Cheese lives
@add_link_field('cheese','milk_food', 'milk')
class FoodAdmin(admin.ModelAdmin):
    list_display = ("id", "...", 'link', 'link2')

很抱歉这个例子太不合逻辑了,但我不想使用我的数据。

【讨论】:

  • 重大改进!很高兴人们正在使用/改进它。
【解决方案4】:

这是我目前的解决方案,基于 Pannu(在他的编辑中)和 Mikhail 的建议。

我有几个顶级管理员更改视图我需要链接到相关对象的顶级管理员更改视图,还有几个内联管理员更改视图我需要链接到顶级管理员更改同一对象的视图。因此,我想将链接方法分解出来,而不是为每个管理员更改视图重复它的变体。

我使用类装饰器创建 link 可调用对象,并将其添加到 readonly_fields

def add_link_field(target_model = None, field = '', link_text = unicode):
    def add_link(cls):
        reverse_name = target_model or cls.model.__name__.lower()
        def link(self, instance):
            app_name = instance._meta.app_label
            reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
            link_obj = getattr(instance, field, None) or instance
            url = reverse(reverse_path, args = (link_obj.id,))
            return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
        link.allow_tags = True
        link.short_description = reverse_name + ' link'
        cls.link = link
        cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + ['link']
        return cls
    return add_link

如果您需要以某种方式获取链接文本,而不仅仅是在要链接到的对象上调用 unicode,您还可以传递自定义可调用对象。

我是这样使用的:

# the first 'blog' is the name of the model who's change page you want to link to
# the second is the name of the field on the model you're linking from
# so here, Post.blog is a foreign key to a Blog object. 
@add_link_field('blog', 'blog')
class PostAdmin(admin.ModelAdmin):
    inlines = [SubPostInline, DefinitionInline]
    fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
    list_display = ('__unicode__', 'enabled', 'link')

# can call without arguments when you want to link to the model change page
# for the model of an inline model admin.
@add_link_field()
class PostInline(admin.StackedInline):
    model = Post
    fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
    extra = 0

当然,如果我可以将SubPostDefinition 的管理员更改视图嵌套在Blog 管理员更改页面上Post 的内联管理员内,而无需修补Django。

【讨论】:

  • 你可以从cls._meta.app_label 得到app_name 和来自cls._meta__str__() 的型号名称,这是很好的尝试,我会使用它。 :)
  • 看起来我可以从instance._meta.app_label 获取站点名称,这很好——我不需要在可调用对象之外使用它,但我仍然需要模型的名称(对于内联版本)在可调用之外设置short_description
  • 感谢您的想法,我想我找到了方法。对于使用它从内联管理员链接到同一模型的管理员更改页面的情况,我从cls.model.__name__ 获得名称。在其他情况下仍然需要手动输入模型名称,因为链接的目标可以是任何相关对象。
  • 我试过用这个,不太了解,因为我是 django 新手,并得到这个错误:渲染时捕获 AttributeError:'XXXXXXXXXXXXAdmin' 对象没有属性'name',
  • 我发现这很有帮助。我进行了编辑,我相信 add_link_field 中的字段应该默认为空字符串,而不是 None。 getattr 需要一个字符串,因此除非默认值为空字符串,否则 getattr 或实例不起作用。
【解决方案5】:

使用readonly_fields:

class MyInline(admin.TabularInline):
    model = MyModel
    readonly_fields = ['link']

    def link(self, obj):
        url = reverse(...)
        return mark_safe("<a href='%s'>edit</a>" % url)

    # the following is necessary if 'link' method is also used in list_display
    link.allow_tags = True

【讨论】:

  • 上面的代码不能作为 mixin 工作,它显然应该省略 model=MyModel 定义并继承自对象而不是 TabularInline。
  • 对我来说,这里的关键信息是您可以在您的 modelAdmin 上使用可调用对象作为字段。我不知何故没有意识到这一点。虽然直到现在我才注意到,但另一个答案实际上是在您发布之前添加了该信息。我要给他赏金。如果您想尝试一个实现,如果它比我提出的 factory-function-generating-a-mixin 更好,我会接受您的回答。
  • 我不关心赏金或接受的答案计数,但是将 html 链接放置到假模型字段的自定义小部件的想法有点疯狂,我认为向未来的 stackoverflowers 建议是不是一个好主意 :) 就我的口味而言,这里也不需要 mixin,而 readonly_fields + 一个可调用的就可以了,因此我不会改变我的答案。因此,也许最好只提供您提出的答案并将其标记为已接受。
  • 我只是不喜欢将方法(以及 link.allow_tagslink.short_description)添加到每个需要它的模型中,因为一些参数从一个模型更改为下一个模型.不要重复自己是软件开发的原则之一。您是说重复次数不足以证明将其排除在外吗?如果我的管理员中有 10 个模型需要它怎么办?还是 20 个?
  • @MikhailKorobov:我同意,这是我在 django 1.1 中使用的旧 sn-p,尽管您的解决方案对于当前版本更好。 :) 然而,创建自定义小部件或向模型表单添加字段并不疯狂,这是 django 的优势之一。老实说,直到我查看了 pastebin 链接,我才明白这个问题的意图。
【解决方案6】:

查看管理类的源代码很有启发性:它表明上下文中有一个对象可用于管理视图,称为“原始”。

这是类似的情况,我需要将一些信息添加到更改列表视图中:Adding data to admin templates(在我的博客上)。

【讨论】:

  • 我不认为这对我来说比添加一个可调用视图更好。我可以轻松地排除该代码,但我希望链接结束的位置位于三个不同的模板中——fieldset.html 用于从对象的管理页面指向相关对象的链接,stacked.html 和 @ 987654324@ 用于链接到内联管理员中对象的顶级管理页面。虽然使用 original 会在一定程度上简化我的路径,但不会减少我需要对模板进行的更改。
【解决方案7】:

我同意很难进行模板编辑,因此我创建了一个自定义小部件以在管理员更改视图页面上显示 anchor(可用于表单和内联表单)。

所以,我使用了锚小部件以及表单覆盖来获取页面上的链接。

forms.py:

class AnchorWidget(forms.Widget):

    def _format_value(self,value):
        if self.is_localized:
            return formats.localize_input(value)
        return value

    def render(self, name, value, attrs=None):
        if not value:
            value = u''

        text = unicode("")
        if self.attrs.has_key('text'):
            text = self.attrs.pop('text')

        final_attrs = self.build_attrs(attrs,name=name)

        return mark_safe(u"<a %s>%s</a>" %(flatatt(final_attrs),unicode(text)))

class PostAdminForm(forms.ModelForm):
    .......

    def __init__(self,*args,**kwargs):
        super(PostAdminForm, self).__init__(*args, **kwargs)
        instance = kwargs.get('instance',None)
        if instance.blog:
            href = reverse("admin:appname_Blog_change",args=(instance.blog))  
            self.fields["link"] = forms.CharField(label="View Blog",required=False,widget=AnchorWidget(attrs={'text':'go to blog','href':href}))


 class BlogAdminForm(forms.ModelForm):
    .......
    link = forms..CharField(label="View Post",required=False,widget=AnchorWidget(attrs={'text':'go to post'}))

    def __init__(self,*args,**kwargs):
        super(BlogAdminForm, self).__init__(*args, **kwargs)
        instance = kwargs.get('instance',None)
        href = ""
        if instance:
            posts = Post.objects.filter(blog=instance.pk)
            for idx,post in enumerate(posts):
                href = reverse("admin:appname_Post_change",args=(post["id"]))  
                self.fields["link_%s" % idx] = forms..CharField(label=Post["name"],required=False,widget=AnchorWidget(attrs={'text':post["desc"],'href':href}))

现在在您的ModelAdmin 中覆盖form 属性,您应该会得到想要的结果。我假设您在这些表之间有OneToOne 关系,如果您有一对多,那么BlogAdmin 侧将不起作用。

更新: 我已经对动态添加链接进行了一些更改,这也解决了BlogPostOneToMany 问题,希望这可以解决问题。 :)

Pastebin 之后: 在您的PostAdmin 我注意到blog_link,这意味着您试图在changelist_view 上显示blog 链接,其中列出了所有帖子。如果我是正确的,那么您应该添加一个方法来在页面上显示链接。

class PostAdmin(admin.ModelAdmin):
    model = Post
    inlines = [SubPostInline, DefinitionInline]
    list_display = ('__unicode__', 'enabled', 'blog_on_site')

    def blog_on_site(self, obj):
        href = reverse("admin:appname_Blog_change",args=(obj.blog))
        return mark_safe(u"<a href='%s'>%s</a>" %(href,obj.desc))
    blog_on_site.allow_tags = True
    blog_on_site.short_description = 'Blog'

至于在BlogAdmin changelist_view 上显示post 链接,您可以执行与上述相同的操作。我之前的解决方案会在change_view 页面向您显示低一级的链接,您可以在其中编辑每个实例。

如果您希望BlogAdmin 页面在change_view 页面中显示指向post 的链接,那么您必须通过覆盖class BlogAdminget_form 方法来动态地将每个链接包含在fieldsets 中并动态添加链接,在get_form 中设置self.fieldsets,但首先不要将元组用于fieldsets,而是使用列表。

【讨论】:

  • 这是一种外键关系,一个博客对多个帖子。但这看起来是一个好的开始,我会在接下来的几天内尝试一下。
  • 我添加了一些更改以动态添加这些链接,这应该可以解决问题。 :)
  • 好的,我刚刚有机会尝试一下。将args=(instance.blog) 更改为args=(instance.blog,) 并将args=(post["id"]) 更改为args=(post.id,) 后,两者都出现相同的错误:NoReverseMatch at ...: Reverse for 'Site_(Blog or Post)_change' with arguments '(the arg here,)' and keyword arguments '{}' not found。我还尝试将我的站点的实际名称放入而不是 Site,但仍然出现相同的错误。有什么想法吗?我在 Django 1.3 上,以防万一。
  • 哦,对不起,Site 实际上是您的应用名称。请用您的应用名称替换Site,它应该可以工作。请也看看这个docs.djangoproject.com/en/dev/ref/contrib/admin/…
  • 我将所有内容都小写了,并将您的所有引用从post['key'] 更改为post.key,它消除了错误。我们正在取得进展。但是,它根本没有改变任何一个管理页面。除了将form = PostAdminForm 添加到我的模型之外,我还需要做其他事情吗?
猜你喜欢
  • 2012-04-10
  • 1970-01-01
  • 2013-10-28
  • 2011-08-19
  • 2018-04-18
  • 1970-01-01
  • 1970-01-01
  • 2015-09-08
  • 2013-01-19
相关资源
最近更新 更多