【问题标题】:How to prevent deletion of Django model from Django Admin, unless part of a cascade如何防止从 Django Admin 中删除 Django 模型,除非是级联的一部分
【发布时间】:2019-12-30 19:53:55
【问题描述】:

我有一个使用 Django 2.2.4 的项目。

我有一个名为 Company 的 Django 模型。

我使用 post_save 信号来确保在创建新公司后立即创建一个名为“Billing”的新模型实例,该实例与该公司相关联。这包含公司的帐单信息。这很好用。

由于我的 Billing 对象与 Company 关联,并且我使用 on_delete=models.CASCADE,因此一旦删除 Company,与该公司关联的 Billing 对象也会自动删除。这也很好用。

由于每个公司的 Billing 对象现在与 Company 一起自动创建和删除,因此使用 Django Admin Web 界面的管理员无需手动创建或删除 Billing 对象。我想对他们隐藏这个功能。

通常,阻止 Django Admin 允许某人添加或删除对象的常用方法是在 admin.py 中将其添加到该模型的 ModelAdmin:

class BillingAdmin(admin.ModelAdmin):
    ...

    # Prevent deletion from admin portal
    def has_delete_permission(self, request, obj=None):
        return False

    # Prevent adding from admin portal
    def has_add_permission(self, request, obj=None):
        return False

这可行,并且确实隐藏了管理员手动创建或删除 Billing 对象实例的能力。然而,它确实有一个负面影响:Django Admin 用户不能再删除公司。删除公司时,Django 会查找所有需要删除的关联对象,通知用户不允许删除关联的 Billing 对象,并阻止用户删除公司。

虽然我不希望 Django Admin 用户能够手动创建或删除 Billing 模型的实例,但我仍然希望他们能够删除整个 Company,这将导致删除与该公司关联的计费模式。

就我而言,阻止用户删除 Billing 模型的实例与其说是一项安全功能,不如说是一项安全功能,因为它旨在防止混淆,不让数据库最终处于存在公司的状态,但是它不存在计费对象。 Django 显然不会有这个问题,但它会让用户感到困惑。

有解决办法吗?

更新:

使用has_delete_permission 设置,如果您尝试通过 Django Admin 删除公司,您会得到:

不抛出异常。至少没有一个未被捕获并出现在 Django 日志中。

我的模型如下所示:

class Company(Group):
    ...

class Billing(models.Model):
    company = AutoOneToOneField('Company', on_delete=models.CASCADE, blank=False, null=False, related_name="billing")
    monthly_rate = models.DecimalField(max_digits=10, decimal_places=2, default=0, blank=False, null=False)

# Create billing object for a company when it is first created
@receiver(post_save, sender=Company)
def create_billing_for_company(sender, instance, created, *args, **kwargs):
    if created:
        Billing.objects.create(company=instance)

AutoOneToOneField 是 django-annoying 的一部分。它确保如果您运行 MyCompany.billing,并且关联的计费对象尚不存在,则会自动创建一个,而不是引发异常。这里可能不需要,因为我在创建公司时会自动创建对象,但这不会造成伤害,并确保我的代码永远不必担心关联的对象不存在。

另外请注意,我没有覆盖我的计费模型的 delete 函数。

【问题讨论】:

  • 意见:你不应该删除任何东西。我的意思是最好的方式,但最终用户不擅长不把事情搞砸。你为什么要让他们删除一个值然后级联它???在模型上添加状态/活动字段并通过将其设置为 0 来“软删除”它。删除保留给 DBA 或最终用户以外的人。
  • @dfundako 我们不是在谈论用户。我们正在谈论管理员。 Django Admin Web 界面的用户。由于 Django Admin Web 界面本质上是一个进入 DB 的界面,因此这些用户或多或少是 DBA。而people are bad at not messing things up 正是我试图阻止他们删除 Billing 对象的原因,该对象应该在公司存在时存在,而在公司被删除时不存在。
  • 恕我直言:我认为@dfundako 是在谈论任何类型的用户,管理员是一种用户,我同意避免删除是最佳做法,管理员的不当行为可能会导致大问题在您的系统中。通过布尔值(活动/非活动)更改删除是最好的方法,如果您的问题是存储限制,您可以构建一个脚本来删除每个月的非活动记录。
  • 我没有无限的时间来处理这个项目。我还有 10 个其他项目等着我。我不会花一周时间开发一个复杂的系统来防止管理员删除公司,因为 Django Admin 中已经有警告告诉管理员确切将删除什么,并在删除任何内容之前强制他们确认。很有可能,在 6 到 12 个月后投入生产时,我将成为使用 Django Admin 的人。除非我确定,否则我知道不要删除公司,但我可能不记得计费模型的详细信息,或者我是否应该添加/删除它。
  • 您想要的只是从模板中完全隐藏“删除”选项,您可以通过复制默认模板并从中删除删除按钮来覆盖Billing 模型的“add form template”。

标签: python django


【解决方案1】:

另一个选项是在主 Company ModelAdmin 中覆盖专门为此方法设计的 get_deleted_objects - 以允许在从管理网络删除公司时删除所有相关对象。

class CompanyAdmin(admin.ModelAdmin):
    def get_deleted_objects(self, objs, request):
        """
        Allow deleting related objects if their model is present in admin_site
        and user does not have permissions to delete them from admin web
        """
        deleted_objects, model_count, perms_needed, protected = \
            super().get_deleted_objects(objs, request)
        return deleted_objects, model_count, set(), protected

这里我们将perms_needed 替换为空的set() - 这是用户通过管理站点删除相关对象时无法满足的一组权限。


当通过 django admin 删除对象时:

  • 检查用户是否有权删除主对象
  • 计算也应删除的其他相关对象的列表
  • 对于这些相关对象,如果它们的模型在 admin_site 中注册,django 会执行额外的权限检查
  • 如果用户有管理员站点权限也可以删除这些相关对象
  • 如果用户无权删除相关对象 - 这些必需的权限将添加到列表中并显示为错误页面

使用主要的一种实用方法获取要删除的相关对象列表 - get_deleted_objects

从 Django 2.1 开始,有更舒适的方法可以直接从 ModelAdmin 实例覆盖它: get_deleted_objects

【讨论】:

    【解决方案2】:

    经过一番挖掘,does appearModelAdmin 将简单地在对象上调用delete(),这意味着它不应该专门查看您的管理员计费权限。 Looking at the model delete 也确认它不关心管理员权限是什么。

    我很好奇,想知道has_delete_permission 函数是否会查看相关对象。 That also didn't appear to be the case。在这一点上,我很好奇你是否覆盖了你的Billing 模型的delete 函数?这将阻止删除,如果您将CASCADE 设置为关系的on_delete,则此时您将无法完成删除Company,因为它无法级联删除。

    如果您有堆栈跟踪或明确的错误消息,请分享。


    话虽如此,我不知道我是否同意这种方法。我认为在Billing 的模型级别执行此操作会更有意义。尝试delete 时,您可以检查Company 是否没有其他Billing 对象,如果是,则引发验证错误,通知用户Company 必须至少有一个Billing。我不知道你的模型,因为它们没有发布,所以如果它是一对一的关系,请忽略这个。以下是我期望它看起来如何的粗略想法:

    def delete(self):
        other_billing = Billing.objects.filter(company_id=self.company.id).exclude(id=self.id).first()
        if not other_billing:
            raise ValidationError({"message": "A company must have at least one Billing."})
        super().delete()
    

    编辑:这是一个使用ModelAdmin.delete_model() 的方法,它不会引发异常。

    def delete_model(self, request, billing):
        other_billing = Billing.objects.filter(company_id=billing.company.id).exclude(id=billing.id).first()
        if not other_billing:
            # from django.contrib import messages
            messages.error(request, "A company must have at least one Billing.")
        else:
            super().delete_model(request, billing)
    

    编辑:我确实发现您可以访问 request,这似乎是通过 has_delete_permissions() 检查您是否在您的管理员更改页面上的唯一可靠方法型号与否。郑重声明我认为这种方式很hacky,我不推荐它。但是,它允许级联删除,而不允许通过更改页面进行删除(它将隐藏按钮):

    def has_delete_permissions(self, request, obj=None):
        # If we have an object, it's been fetched for deletion or to check permission against it.
        if isinstance(obj, Billing):
            if request.path == reverse("admin:<APP_NAME>_billing_change", args=[obj.id]):
                return False
    
        return True
    

    【讨论】:

    • delete() 方法中引发的ValidationError 通常只会“崩溃”无论您在做什么(视图将返回 500 错误),因为验证错误不会被捕获等级。只有在模型上运行 clean() 时才会捕获它们。
    • @dirkgroten 啊,有趣。我不必真正尝试这样做,所以很高兴知道。那么在这里使用clean() 有意义吗?我知道它通常被用作save() 的一部分。
    • 通过ModelAdmin 删除模型时,不会调用模型的clean() 方法,因此无济于事。
    • 不,它不是从save() 调用的,但在提交表单并清理它时总是会调用它。这也是捕获验证的地方 (form.is_valid())。问题是对于删除操作,没有表单验证。
    • 但是您可以使用ModelAdmin.delete_model() 方法来实现这一点。只有在有另一个计费时才调用super() 方法,否则,什么也不做,在传递给delete_modelrequest 中添加一条错误消息。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-17
    • 2013-01-17
    • 2013-01-07
    • 2011-04-18
    相关资源
    最近更新 更多