在 Django 管理界面中移除“添加另一个”的选项

41
无论何时我在编辑具有外键到对象B的对象A,都会在对象B的选项旁边提供加号选项“添加另一个”。如何删除该选项?
我配置了一个没有添加对象B权限的用户。加号仍然可用,但当我单击它时,它会显示“权限被拒绝”。这很丑。
我正在使用Django 1.0.2

1
相关(但不同)问题:https://dev59.com/oW855IYBdhLWcg3wynmC - user9876
3
现在可以了!max_num=0 - andilabs
@andi 这个解决方案与pistache的回答类似,但不适用于OP的情况(请参见我在pistache的回答下的评论以了解详情)。这个问题类似于这个相关问题的**#1**红色圆圈:https://dev59.com/kF8d5IYBdhLWcg3wzEz4 - Ad N
请查看 ModelAdmin.has_add_permission(request):https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.has_add_permission - guettli
12个回答

68
以下回答是我最初的回答,但它是错误的,没有回答OP的问题:
更简单的解决方案,无需CSS hack,也不需要编辑Django代码库:
将以下内容添加到您的Inline类中:
max_num=0

(这仅适用于内联表单,而不是 OP 所要求的外键字段)


上面的答案只对内联表单中隐藏“添加相关”的按钮有用,不适用于外键。

当我写答案时,我记得接受的答案同时隐藏了两个按钮,这就是我感到困惑的原因。

以下内容似乎提供了解决方案(尽管使用 CSS 隐藏似乎是最可行的方法,特别是如果 FK 的“添加另一个”按钮在内联表单中):

Django 1.7 移除内联表单中的“添加”按钮


6
你能提供更多信息和可能的例子吗?这似乎不能解决原帖作者的问题。你需要使用InlineAdmin将A内嵌到B的管理页面中。但是,原帖作者想要防止在A的管理页面中创建B对象。在文档中,你可以看到Book在Author上有一个ForeignKey,但是你将Book内嵌到Author中,而不是将Author内嵌到Book中。https://docs.djangoproject.com/en/1.1/ref/contrib/admin/#inlinemodeladmin-objects - Endophage
4
这并不是 OP 问题的确切答案。 - frnhr
为什么?至少在我编写它的时候,它可以删除内联表单中的“添加另一个”链接。 - pistache
2
@pistache 这不是 OP 问题的答案,因为在他的情况下,A 有一个指向 B 的外键,所以一个 A 对象只能有 一个B 的引用,并且他正在引用 A 表单中 B 字段旁边的 **+**。基本上,如果您参考另一个 问题 中的图像,OP 想要删除 #1 红色圆圈中的加号,而您的答案则删除了 #3 或 #4。 - Ad N
是的,你们都非常正确,这个答案完全是垃圾,离OP所问的问题很远。这是很久以前写的,我想当时我对Django的表单理解并不像我想象的那么好。对于造成的困惑,我感到抱歉。 - pistache
显示剩余2条评论

32
尽管这里提到的大多数解决方案都有效,但有另一种更简洁的方法可以实现。可能是在其他解决方案被介绍后,Django 的较新版本中引入的。(我目前正在使用 Django 1.7)
要移除“添加另一个”选项,
class ... #(Your inline class)

    def has_add_permission(self, request):
        return False

类似地,如果您想禁用“删除?”选项,请在Inline类中添加以下方法。

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

3
在大多数情况下,这不是正确的解决方案——它不仅会删除ForeignKey字段前面的小的“添加”、“删除”符号,而且还会从管理界面的所有位置中删除关联模型的“添加/编辑”选项。 - Nitin Nain
2
我认为这是正确的,因为它是InlineModelAdmin的子类(而不是ModelAdmin),需要显式地添加到其他ModelAdmininlines中。让我修复has_add_permission,它需要obj参数(def has_add_permission(self, request, obj):)。 - okapies

15

注意:适用于DJango 1.5.2版本以及可能更旧的版本。 can_add_related属性出现在大约两年前。

我发现的最好方法是覆盖您的ModelAdmin的get_form函数。 在我的情况下,我想强制帖子的作者成为当前登录的用户。 下面是代码和丰富的注释。 真正重要的部分是设置widget.can_add_related

def get_form(self,request, obj=None, **kwargs):
    # get base form object    
    form = super(BlogPostAdmin,self).get_form(request, obj, **kwargs)

    # get the foreign key field I want to restrict
    author = form.base_fields["author"]

    # remove the green + by setting can_add_related to False on the widget
    author.widget.can_add_related = False

    # restrict queryset for field to just the current user
    author.queryset = User.objects.filter(pk=request.user.pk)

    # set the initial value of the field to current user. Redundant as there will
    # only be one option anyway.
    author.initial = request.user.pk

    # set the field's empty_label to None to remove the "------" null 
    # field from the select. 
    author.empty_label = None

    # return our now modified form.
    return form

在这里进行更改的有趣部分是,get_form 中的 author.widgetdjango.contrib.admin.widgets.RelatedFieldWidgetWrapper 的实例,而如果您尝试在其中一个 formfield_for_xxxxx 函数中进行更改,则小部件是实际表单小部件的实例,在这种典型的 ForeignKey 情况下,它是一个 django.forms.widgets.Select


你知道如何让这个适用于 InlineModelAdmin 吗? - Ad N
抱歉,我从未尝试过在“InlineModelAdmin”中使用它。虽然我认为它应该是类似的,但我不确定如何覆盖“InlineModelAdmin”,而且自从发布这篇文章后,我就没有编写过任何Django特定的Python代码了。 - Endophage
@Ad N 请查看我下面关于InlineModelAdmin的答案。 - Slipstream

14

我使用以下方式处理 FormInlineForm

Django 2.0,Python 3+

Form

class MyModelAdmin(admin.ModelAdmin):
    #...
    def get_form(self,request, obj=None, **kwargs):

        form = super().get_form(request, obj, **kwargs)
        user = form.base_fields["user"]

        user.widget.can_add_related = False
        user.widget.can_delete_related = False
        user.widget.can_change_related = False

        return form  

内联表单

class MyModelInline(admin.TabularInline):
    #...
    def get_formset(self, request, obj=None, **kwargs):

        formset = super().get_formset(request, obj, **kwargs)
        user = formset.form.base_fields['user']

        user.widget.can_add_related = False
        user.widget.can_delete_related = False
        user.widget.can_change_related = False

        return formset

11
@Slipstream的答案展示了如何实现解决方案,即通过重写formfield的widget属性来实现,但是,在我看来,get_form不是最合适的地方。 @cethegeek的答案展示了在formfield_for_dbfield的扩展中实现解决方案的位置,但没有提供明确的示例。
为什么要使用formfield_for_dbfield?它的文档字符串表明它是用于处理表单字段的指定钩子:

指定给定数据库字段实例的表单Field实例的钩子。

它还允许(稍微)更清晰、更简洁的代码,并且我们可以轻松设置其他表单Field 属性,例如initial值和/或disabled(示例在这里),将它们添加到kwargs(在调用super之前)。
因此,结合这两个答案(假设OP的模型是ModelAModelB,并且ForeignKey模型字段名为b):
class ModelAAdmin(admin.ModelAdmin):
    def formfield_for_dbfield(self, db_field, request, **kwargs):
        # optionally set Field attributes here, by adding them to kwargs
        formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
        if db_field.name == 'b':
            formfield.widget.can_add_related = False
            formfield.widget.can_change_related = False
            formfield.widget.can_delete_related = False
        return formfield

# Don't forget to register...
admin.site.register(ModelA, ModelAAdmin)

注意:如果 ForeignKey 模型字段有 on_delete=models.CASCADE,那么 can_delete_related 属性默认为 False,可以在 RelatedFieldWidgetWrapper源代码 中看到。


6

请查看 django.contrib.admin.options.py 文件并查看 BaseModelAdmin 类,formfield_for_dbfield 方法。

您将看到以下内容:

# 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:
    formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)

我认为最好的方法是创建ModelAdmin的子类(它又是BaseModelAdmin的子类),以此为基础创建你的模型,覆盖formfield_fo_dbfield并使其不会或有条件地包装小部件在RelatedFieldWidgetWrapper中。
有人可能会说,如果用户没有添加相关对象的权限,RelatedFieldWidgetWrapper不应该显示添加链接?也许这是值得在Django trac中提及的事情?

2
+1 这是最好的答案。它不是 CSS hack,也不会破坏小部件在其他情况下的功能。您可以为您想要的 ModelAdmins 子类化,同时保留其他小部件不变。 - JCotton

4

不推荐使用的答案

Django现在已经可以实现这个功能了。


你考虑过使用CSS来隐藏按钮吗?也许这有点hacky。

我没有测试过,但我想...

no-addanother-button.css

#_addanother { display: none }

admin.py

class YourAdmin(admin.ModelAdmin):
    # ...
    class Media:
        # edit this path to wherever
        css = { 'all' : ('css/no-addanother-button.css',) }

这是有关进行此操作的Django文档--作为静态定义的媒体

注意/编辑: 文档中说文件将以MEDIA_URL为前缀,但在我的实验中并没有。您的情况可能有所不同。

如果您发现这也是您的情况,这里有一个快速修复方法...

class YourAdmin(admin.ModelAdmin):
    # ...
    class Media:
        from django.conf import settings
        media_url = getattr(settings, 'MEDIA_URL', '/media/')
        # edit this path to wherever
        css = { 'all' : (media_url+'css/no-addanother-button.css',) }

2
不需要使用肮脏的黑客技巧,请参见下面pistache的答案。 - Thomas Parslow
@ThomasParslow 我添加了一个大而粗的注释,因为我的答案现在已经过时了。 - T. Stone
1
@T.Stone 如果您仔细阅读OP的问题并查看内联类的文档,您会发现pistache的答案实际上并没有解决问题。我已经添加了一个解决方案。 - Endophage
@Endophage 你说得对,它不适用于 OP 的情况(请看我在 pistache 回答下的评论)。 - Ad N
您可以查看我下面提供的Form和InlineForm解决方案。 - Slipstream

1
我正在使用 Django 2.x,我认为我找到了最好的解决方案,至少对于我的情况来说。
“保存并添加另一个”按钮的 HTML 文件在 your_python_installation\Lib\site-packages\django\contrib\admin\templates\admin\submit_line.html
  1. 将该 HTML 文件复制并粘贴到您的项目中,如下所示:your_project\templates\admin\submit_line.html
  2. 打开它并根据需要注释/删除按钮代码:

{#{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}#}

我知道这个问题已经有答案了。但也许将来会有人遇到与我类似的情况。

0

基于cethegeek的答案,我做了这个:

class SomeAdmin(admin.ModelAdmin):
    form = SomeForm

    def formfield_for_dbfield(self, db_field, **kwargs):
        formfield = super(SomeAdmin, self).formfield_for_dbfield(db_field, **kwargs)
        if db_field.name == 'some_m2m_field':
            request = kwargs.pop("request", None)
            formfield = self.formfield_for_manytomany(db_field, request, **kwargs)  # for foreignkey: .formfield_for_foreignkey
            wrapper_kwargs = {'can_add_related': False, 'can_change_related': False, 'can_delete_related': False}
            formfield.widget = admin.widgets.RelatedFieldWidgetWrapper(
                formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs
            )
        return formfield

你能解释一下为什么/如何它是更好的解决方案吗?它看起来只是更加复杂。 - Sebastián Vansteenkiste

0

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