如何防止在 Django Admin 中对 FK / MTM 字段进行自我(递归)选择

Posted

技术标签:

【中文标题】如何防止在 Django Admin 中对 FK / MTM 字段进行自我(递归)选择【英文标题】:How to prevent self (recursive) selection for FK / MTM fields in the Django Admin 【发布时间】:2010-10-26 13:44:33 【问题描述】:

给定一个具有 ForeignKeyField (FKF) 或 ManyToManyField (MTMF) 字段的模型,其中外键为“self”,我如何防止在 Django Admin (admin) 中进行 self(递归)选择。

简而言之,应该可以防止在管理员中自我(递归)选择模型实例。这适用于编辑模型的现有实例,而不是创建新实例。

例如,新闻应用中的文章采用以下模型;

class Article(models.Model):           
    title = models.CharField(max_length=100)
    slug = models.SlugField()
    related_articles = models.ManyToManyField('self')

如果有 3 个 Article 实例(标题:a1-3),当通过管理员编辑现有的 Article 实例时,related_articles 字段默认由 html(多个)选择框表示,该框提供所有文章列表 (Article.objects.all())。用户应该只能看到并能够选择 Article 实例而不是它自己,例如编辑时Article a1, related_articles 可供选择= a2, a3。

我目前可以看到 3 种可能的方法来做到这一点,按偏好递减的顺序排列;

    提供一种方法来设置查询集,在管理表单字段中为related_articles 提供可用选项(通过排除查询过滤器,例如Article.objects.filter(~Q(id__iexact=self.id)),以从用户可以看到的相关文章列表中排除当前正在编辑的实例并从中选择。要使用的查询集的创建/设置可能发生在自定义 Article ModelForm 的构造函数 (__init__) 中,或者通过某种动态 limit_choices_to Model 选项发生。这需要一种获取实例的方法正在编辑以用于过滤。 覆盖Article ModelModelAdmin 类的save_model 函数以在保存实例之前检查并从related_articles 中删除自身。这仍然意味着管理员用户可以查看和选择所有文章,包括正在编辑的实例(对于现有文章)。 当需要在管理员之外使用时过滤掉自我引用,例如模板。

理想的解决方案 (1) 目前可以通过管理外部的自定义模型表单来完成,因为可以将正在编辑的实例的过滤查询集变量传递给模型表单构造函数。问题是,您能否访问 Article 实例,即在创建表单之前正在编辑“自我”以执行相同的操作。

这可能是我的处理方式错误,但如果您允许将 FKF / MTMF 定义为同一模型,那么应该有一种方法让管理员 - 做正确的事 - 并通过将用户排除在可用选项列表中来防止用户选择自己。

注意:现在可以执行解决方案 2 和 3,并且提供解决方案 2 和 3 是为了尽量避免将这些作为答案,理想情况下我希望得到解决方案 1 的答案。

【问题讨论】:

【参考方案1】:

我喜欢save()时间检查的解决方案:

    def save(self, *args, **kwargs):
        # call full_clean() that in turn will call clean()
        self.full_clean()
        return super().save(*args, **kwargs)

    def clean(self):
        obj = self
        parents = set()
        while obj is not None:
            if obj in parents:
                raise ValidationError('Loop error', code='infinite_loop')
            parents.add(obj)
            obj = obj.parent

【讨论】:

【参考方案2】:

您还可以像这样覆盖 ModelAdmin 的 get_form 方法:

def get_form(self, request, obj=None, **kwargs):
    """
    Modify the fields in the form that are self-referential by
    removing self instance from queryset
    """
    form = super().get_form(request, obj=None, **kwargs)
    # obj won't exist yet for create page
    if obj:
        # Finds fieldnames of related fields whose model is self
        rmself_fields = [f.name for f in self.model._meta.get_fields() if (
            f.concrete and f.is_relation and f.related_model is self.model)]
        for fieldname in rmself_fields:
            form.base_fields[fieldname]._queryset =
                form.base_fields[fieldname]._queryset.exclude(id=obj.id)
    return form

请注意,这是一个万能的解决方案,它会自动查找自引用模型字段并从所有这些字段中删除 self :-)

【讨论】:

【参考方案3】:

Carl 是正确的,这是一个剪切和粘贴代码示例,可以放入 admin.py

如果你没有扎实的掌握,我发现导航 Django 关系可能会很棘手,一个活生生的例子可能比“阅读这个”更有价值 1000 倍(并不是说你不需要了解什么正在发生)。

class MyForm(forms.ModelForm):
    class Meta:
        model = MyModel

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        self.fields['myManyToManyField'].queryset = MyModel.objects.exclude(
            id__exact=self.instance.id)

【讨论】:

【参考方案4】:

您可以在管理员中使用自定义 ModelForm(通过设置 the "form" attribute of your ModelAdmin subclass)。因此,您在管理员中的操作方式与在其他任何地方相同。

【讨论】:

技术上,我知道为什么limit_choices_to 不能这样做。实际上,我不明白为什么它从未实施过。 @kojiro - 简单的事情应该是简单的,高级的事情应该是可能的。并非所有可能的功能都必须压缩到 limit_choices_to 中,正是因为您始终可以编写自己的 ModelForm 代码来做任何您想做的事情。如果您有关于将其添加到 limit_choices_to 而不使其更复杂的直观语法的建议,欢迎您打开带有补丁的工单并提出建议。

以上是关于如何防止在 Django Admin 中对 FK / MTM 字段进行自我(递归)选择的主要内容,如果未能解决你的问题,请参考以下文章

如何在django admin中对不存在的字段进行排序/排序[重复]

Django admin:从反向 FK 聚合中排序 list_display 字段

django上课笔记5

Django Admin,修改/自定义manytomany字段的选择框中的名称

Django Admin后台添加用户时出现报错:1452

如何在 django admin change_form 中实现 pygment 语法高亮?