在Django的管理界面中禁用编辑对象的链接(仅显示列表)?

56
在Django的管理后台中,我想要禁用“选择要更改的项目”页面上提供的链接,这样用户就无法进入编辑该项目的任何地方。(我将限制用户对此列表所能进行的操作为一组下拉操作 - 无法实际编辑字段)。
我发现Django有能力选择哪些字段显示链接,但我无法看到如何不显示任何字段链接。
class HitAdmin(admin.ModelAdmin):
    list_display = ('user','ip','user_agent','hitcount')
    search_fields = ('ip','user_agent')
    date_hierarchy = 'created'
    list_display_links = [] # doesn't work, goes to default

有什么办法可以获得我的对象列表,而不带有任何编辑链接?

4
在Django 1.11中,list_display_links = None 禁用了显示链接。 - sam
4
在Django 2.0中,只需使用以下代码:list_display_links = None。 - amine.b
13个回答

64

我想要一个仅以列表形式呈现的日志查看器。

我通过以下方式使其正常工作:

class LogEntryAdmin(ModelAdmin):
    actions = None
    list_display = (
        'action_time', 'user',
        'content_type', 'object_repr', 
        'change_message')

    search_fields = ['=user__username', ]
    fieldsets = [
        (None, {'fields':()}), 
        ]

    def __init__(self, *args, **kwargs):
        super(LogEntryAdmin, self).__init__(*args, **kwargs)
        self.list_display_links = (None, )

这有点像两个答案的混合。

如果你只是执行self.list_display_links = (),它将显示链接。但因为template-tag代码(templatetags/admin_list.py)会再次检查列表是否为空,所以任何方式都可以。


2
刚看到你的帖子,这个方法对我也有效(在__init__中设置self.list_display_links = (None,))。谢谢! - thornomad
3
所有答案都没有提供实际禁止更改日志条目的方法。虽然用户看不到链接,但是他可以通过键入编辑表单的URL来访问编辑表单并更改条目。用户必须具有“can_change”权限才能查看更改列表视图。这会引入严重的安全漏洞。 - onurmatik
2
@omat:你可以重写ModelAdmin.change_view方法,以便在有人试图手动访问页面时将其重定向到changelist页面或其他任何你想要的地方。下面我会提供一个示例。 - Chris Pratt
2
不幸的是,这在Django 2.2中似乎不起作用。现在Django会给出错误'list_display_links [0]'的值引用了'None',在'list_display'中未定义。 - Cerin
2
@Cerin 只需要使用 list_display_links = None - NoName
显示剩余2条评论

45

正确地完成这个任务需要两个步骤:

  • 隐藏编辑链接,以免有人误操作进入详细页面(更改视图)。
  • 修改更改视图以重定向回列表视图。

第二部分非常重要:如果您不这样做,那么人们仍然可以通过直接输入URL访问更改视图(您可能不希望如此)。这与OWASP所谓的"不安全的直接对象引用"密切相关。

作为答案的一部分,我将构建一个ReadOnlyMixin类,该类可用于提供所有显示的功能。

隐藏编辑链接

Django 1.7使这变得非常容易:您只需将list_display_links设置为None即可。

class ReadOnlyMixin(): # Add inheritance from "object" if using Python 2
    list_display_links = None

"Django 1.6(以及之前的版本)并不那么简单。对于这个问题,有很多答案建议重写__init__以便在对象构造后设置list_display_links,但这使得重用更加困难(我们只能重写构造函数一次)。
我认为更好的选择是按照以下方式重写Django的get_list_display_links方法:"
def get_list_display_links(self, request, list_display):
    """
    Return a sequence containing the fields to be displayed as links
    on the changelist. The list_display parameter is the list of fields
    returned by get_list_display().

    We override Django's default implementation to specify no links unless
    these are explicitly set.
    """
    if self.list_display_links or not list_display:
        return self.list_display_links
    else:
        return (None,)

这使得我们的mixin易于使用:默认情况下隐藏编辑链接,但允许我们在特定的管理员视图中添加它。

重定向到列表视图

我们可以通过覆盖change_view方法来更改详细页面(更改视图)的行为。以下是Chris Pratt建议的技术的扩展,它可以自动找到正确的页面:

enable_change_view = False

def change_view(self, request, object_id, form_url='', extra_context=None):
    """
    The 'change' admin view for this model.

    We override this to redirect back to the changelist unless the view is
    specifically enabled by the "enable_change_view" property.
    """
    if self.enable_change_view:
        return super(ReportMixin, self).change_view(
            request,
            object_id,
            form_url,
            extra_context
        )
    else:
        from django.core.urlresolvers import reverse
        from django.http import HttpResponseRedirect

        opts = self.model._meta
        url = reverse('admin:{app}_{model}_changelist'.format(
            app=opts.app_label,
            model=opts.model_name,
        ))
        return HttpResponseRedirect(url)

这是可定制的 - 通过将enable_change_view切换为True,您可以重新打开详细页面。

删除“添加ITEM”按钮

最后,您可能希望覆盖以下方法以防止人们添加或删除新项目。

def has_add_permission(self, request):
    return False

def has_delete_permission(self, request, obj=None):
    return False

这些更改将会:
  • 禁用“添加 项目”按钮
  • 防止人们通过在URL后添加/add来直接添加项目
  • 防止批量删除

最后,您可以通过修改actions参数来删除“删除所选 项目”操作。

将所有内容放在一起

这是完成的mixin:

from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect

class ReadOnlyMixin(): # Add inheritance from "object" if using Python 2

    actions = None

    enable_change_view = False

    def get_list_display_links(self, request, list_display):
        """
        Return a sequence containing the fields to be displayed as links
        on the changelist. The list_display parameter is the list of fields
        returned by get_list_display().

        We override Django's default implementation to specify no links unless
        these are explicitly set.
        """
        if self.list_display_links or not list_display:
            return self.list_display_links
        else:
            return (None,)

    def change_view(self, request, object_id, form_url='', extra_context=None):
        """
        The 'change' admin view for this model.

        We override this to redirect back to the changelist unless the view is
        specifically enabled by the "enable_change_view" property.
        """
        if self.enable_change_view:
            return super(ReportMixin, self).change_view(
                request,
                object_id,
                form_url,
                extra_context
            )
        else:
            opts = self.model._meta
            url = reverse('admin:{app}_{model}_changelist'.format(
                app=opts.app_label,
                model=opts.model_name,
            ))
            return HttpResponseRedirect(url)

    def has_add_permission(self, request):
        return False

    def has_delete_permission(self, request, obj=None):
        return False

1
仅供未来读者参考:本答案中使用的 super() 语法是 Py2 语法。使用 Py3 的 super() 语法,这个混合类会更易于在任何模型中重用,因为您不需要在 super() 调用中作为属性包含类名,它只是 super().change_view(...) - Bryson
这真的是最全面和棒极了的帖子!我建议为逻辑构建一个mixin,这样您就可以重用它并覆盖change_view,例如def change_view(self, *args, **kwargs),以使其更具弹性,以应对未来的变化 :) - Ron
我将我的mixin作为单独的答案添加了进去,如果有人需要配置和可重用性:https://dev59.com/sXI-5IYBdhLWcg3w6c-1#69461048 - Ron

24
在Django 1.7及更高版本中,你可以这样做
class HitAdmin(admin.ModelAdmin):
    list_display_links = None

确认此方法适用于 Django 4.0。这正是我所需要的! - Rahmat Nazali Salimi

20

如用户omat在上面的评论中提到的,仅仅删除链接并不能阻止用户手动访问修改页面。不过,解决这个问题也很简单:

class MyModelAdmin(admin.ModelAdmin)
    # Other stuff here
    def change_view(self, request, obj=None):
        from django.core.urlresolvers import reverse
        from django.http import HttpResponseRedirect
        return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))

1
这种方法很棒,因为它允许在执行“禁止”之前与request对象进行交互。因此,例如可以基于request.user或者request.session来实现限制。 - JWL

8
在您的模型管理中设置:
list_display_links = (None,)

应该可以了。(至少在1.1.1版本中有效。)

文档链接:list_display_links


1
嗯 - 我正在使用主干,当我尝试时出现 TypeError: getattr(): attribute name must be string [02/Nov/2009 12:05:22] "GET /admin/ HTTP/1.1" 500 2524 错误。有趣。 - thornomad
嗯,有趣。我刚刚检查了我的项目,在那里我正在做这个,它实际上正在运行1.1(r11602)。我刚刚尝试将我的项目升级到trunk(r11706),它似乎仍然可以正常工作。不过,我还有一些其他的管理工作正在进行中(您可以在此处查看详细信息http://joshourisman.com/2009/10/15/django-admin-awesomeness/),我的ModelAdmin设置了list_display_links为(None,),但实际上并没有在我的admin.py中... 我不明白这会对此造成什么影响。 - Josh Ourisman
很奇怪,你的代码可以运行;不确定其中有什么区别......但是,每次都会指向那行代码的错误。奇怪。 - thornomad
我已经尝试过使用完全空的modelAdmin,只有:list_display_links =(None,),但仍然遇到相同的TypeError...如果它对您有效,我觉得我一定做错了什么。 - thornomad
是的,你不能在ModelAdmin本身上特别执行此操作。在ModelAdmin.__init__中对其值进行检查。相反,您必须覆盖__init__,调用super,然后设置list_display_links =(None,)。请参见上面Frederico的答案。 - Chris Pratt
显示剩余2条评论

6

在您的管理页面中,只需写入 list_display_links = None


这确实需要一些澄清!这也是与@sam在2017年8月相同的评论。 - Matthieu Brucher

4

仅供参考,您可以修改changelist_view:

class SomeAdmin(admin.ModelAdmin):
    def changelist_view(self, request, extra_context=None):
        self.list_display_links = (None, )
        return super(SomeAdmin, self).changelist_view(request, extra_context=None)

这对我来说很好用。

4
在Django的更新版本中(至少从1.9开始),可以简单地确定管理类别上的添加、修改和删除权限。请参阅Django管理文档以获取参考信息。以下是一个示例:
@admin.register(Object)
class Admin(admin.ModelAdmin):

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False

2

目前没有官方支持的方法来实现这个。

从代码上看,如果你没有设置 ModelAdmin.list_display_links 的话,它会自动将其设置为第一个元素。因此最简单的方法可能是在你的 ModelAdmin 子类中重写 __init__ 方法,在初始化时取消该属性的设置:

class HitAdmin(admin.ModelAdmin):
    list_display = ('user','ip','user_agent','hitcount')
    search_fields = ('ip','user_agent')
    date_hierarchy = 'created'

    def __init__(self, *args, **kwargs):
        super(HitAdmin, self).__init__(*args, **kwargs)
        self.list_display_links = []

经过初步测试,这似乎有效。但是我不能保证它不会在其他地方出现问题,或者在未来的Django更改中被破坏。

评论后编辑:

无需修补源代码,这将起作用:

    def __init__(self, *args, **kwargs):
        if self.list_display_links:
            unset_list_display = True
        else:
            unset_list_display = False
        super(HitAdmin, self).__init__(*args, **kwargs)
        if unset_list_display:
            self.list_display_links = []

但我非常怀疑任何补丁都不会被Django接受,因为这会打破目前代码明确执行的某些操作。


谢谢你,它对我也起作用。你看到有明显/Pythonic的方法来修补源代码,说“如果子项将其设置为空,请不要做任何事情//但是如果子项根本没有设置它,则要做一些事情”?可以提交它作为一个补丁...但是在这一点上,我所有的修补想法都相当hackish。 - thornomad
谢谢 - 如果他们在ModelAdmin中添加了另一个变量,比如您建议的unset_list_display_links [True/False],那么他们可以在同一if语句中检查它,就像他们正在检查list_display_links一样...然后再次,他们可能只是建议按照您所做的方式进行覆盖。 - thornomad
仔细检查后,我注意到链接现在已添加到选择框中 - 因此,当您选择一个项目时,它会打开页面!唉。 - thornomad
这是 Django(有时令人恼火的)管理策略的一部分:用户应该受到信任。因此,没有内置的方法来限制用户本来可以访问的内容。补丁无疑会被拒绝,但仍然有绕过它的方法。 - Chris Pratt

1

如果你不想费心覆盖init,你也可以采用极其简单粗暴的方法,并为第一个元素提供一个基本上看起来像这样的值:

</a>My non-linked value<a>

我知道,我知道,这不是很漂亮,但也许我们只是改变标记,所以不用太担心在其他地方弄坏什么。

以下是一些关于如何实现这个功能的示例代码:

class HitAdmin(admin.ModelAdmin):
    list_display = ('user_no_link','ip','user_agent','hitcount')

    def user_no_link(self, obj):
        return u'</a>%s<a>' % obj
    user_no_link.allow_tags = True
    user_no_link.short_description = "user"

附带说明:您也可以通过返回return u'%s' % obj.get_full_name()来提高输出的可读性(因为您不希望它成为一个链接),根据您的使用情况可能有点不错。


这也是一种有趣的方法 - 尽管上述的 __init__ 方法可能会破坏它,但它似乎更加直观......但这给了我一些想法,谢谢。 - thornomad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接