如何在Django Admin中防止删除Django模型,除非是级联删除的一部分。

8
我有一个使用Django 2.2.4的项目。 我的Django模型叫做Company。 我使用post_save信号来确保一旦创建新的Company,就会创建一个名为"Billing"的新模型实例,并将其与该公司关联。其中包含公司的计费信息。这很好地起到作用。 由于我的Billing对象与Company相关联,并且我使用了on_delete=models.CASCADE,所以一旦删除公司,与该公司相关联的Billing对象也将自动删除。这也很好地起到作用。 由于每个公司的Billing对象现在都会随着公司的自动创建和删除,因此,管理员使用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

这样做可以隐藏管理员手动创建或删除计费对象实例的能力,但有一个负面影响:Django管理员用户无法再删除公司。当删除公司时,Django会查找所有需要一并删除的关联对象,注意到用户无权删除关联的计费对象,并阻止用户删除公司。
虽然我不希望Django管理员用户手动创建或删除Billing模型的实例,但我仍希望他们能够删除整个公司,这将导致与该公司相关联的Billing模型实例被删除。
在我的情况下,防止用户删除Billing模型实例并不是一项安全功能,而是旨在通过不让数据库处于存在公司但不存在计费对象的状态来防止混乱。显然,Django不会对此感到困惑,但它可能会让用户感到困惑。
有没有解决方法?
更新:
设置了“has_delete_permission”后,如果您尝试通过Django Admin删除公司,则会收到以下消息:

enter image description here

没有抛出任何异常。至少没有未被捕获的异常,并出现在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,并且尚未存在相关联的billing对象,则会自动创建一个对象,而不会引发异常。这里可能不需要,因为我在创建公司时会自动创建对象,但这不会有害,并确保我的代码永远不需要担心相关对象不存在。
还要注意,我没有覆盖我的Billing模型的delete函数。

观点:无论如何,您都不应该删除东西。我的意思是以最友好的方式表达,但最终用户很容易搞砸事情。为什么要让他们删除一个值然后级联它?在模型上添加状态/活动字段,并通过将其设置为0来进行“软删除”。删除应该由DBA或其他非最终用户的人员执行。 - dfundako
2
@dfundako 我们不是在谈论用户,而是在谈论管理员。Django Admin Web界面的用户。由于Django Admin Web界面本质上是一个DB接口,所以这些用户或多或少都是DBA。而且“人们很容易搞砸事情”的事实正是我试图防止他们删除Billing对象的原因,该对象应该在公司存在时存在,在公司被删除时不存在。 - John
在我看来:我认为@dfundako正在谈论任何类型的用户,管理员是一种用户,我同意避免删除是最佳实践,管理员的错误操作可能会在您的系统中引起严重问题。将删除更改为布尔值(活动/非活动)是最佳方法,如果您的问题是存储限制,您可以编写一个脚本,例如每个月删除非活动记录。 - neosergio
1
我没有无限的时间来完成这个项目。 我有其他10个等着我去做的项目。 当Django Admin已经提供了告知管理员将删除的具体内容并强制确认,防止管理员删除公司的复杂系统需要花费一周时间时,我不会这样做。很可能,在6-12个月后投入生产时,我会成为使用Django Admin的人之一。我知道除非我确定,否则不要删除公司,但我可能不记得Billing模型的详细信息或是否应添加/删除它。 - John
3
你想要的只是从模板中完全隐藏“删除”选项,你可以通过复制默认模板并从中移除删除按钮来重写Billing模型的“添加表单模板” 。 - dirkgroten
2个回答

8
另一个选择是覆盖专门为此方法设计的主要 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


4
经过一番调查,似乎 ModelAdmin 只会在对象上调用 delete() 方法,这意味着它不应该专门考虑您的管理员账户的计费权限。查看模型删除 也证实它并不关心管理员权限是什么。
我很好奇,想知道 has_delete_permission 函数是否查看相关对象。 这似乎不是这种情况。此时,我想知道您是否覆盖了 Billing 模型的 delete 函数?这将防止删除,并且如果您将 CASCADE 设置为关系的 on_delete,则此时它将不允许您完成删除 Company,因为无法进行级联删除。
如果您有堆栈跟踪或明确的错误消息,请分享。

话虽如此,我不确定我是否同意这种方法。我认为更有意义的做法是在Billing模型级别强制执行此操作。在尝试删除时,您可以检查是否没有其他CompanyBilling对象,如果是,则引发验证错误,通知用户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()

编辑:这里有一种使用{{link1: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()检查您是否在管理模型的更改页面上。记录一下,我认为这种方法很粗糙,不建议使用。但是,它可以允许级联删除,同时不允许通过更改页面进行删除(它将隐藏按钮):

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

当通过ModelAdmin删除模型时,模型的clean()方法不会被调用,因此这并没有帮助。 - dirkgroten
你可以手动调用delete()方法。它不会被save()自动调用。 - aredzko
1
不,它不是从 save() 调用的,但它总是在提交表单并清理表单时调用。这也是验证被捕获的地方 (form.is_valid())。问题在于对于删除操作,没有表单验证。 - dirkgroten
1
但是您可以使用ModelAdmin.delete_model()方法来实现。仅在存在其他账单时调用super()方法,否则不执行任何操作,并向传递到delete_modelrequest添加一个带有错误消息的信息。 - dirkgroten
感谢您的编辑和见解@dirkgroten。我很少添加自定义管理表单逻辑(或者说不再使用Django模型表单)。谢谢! - aredzko
显示剩余6条评论

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