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

Joh*_*ohn 5 python django

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

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

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

由于我的 Billing 对象与公司相关联,并且我使用on_delete=models.CASCADE,因此一旦公司被删除,与该公司关联的 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
Run Code Online (Sandbox Code Playgroud)

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

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

就我而言,阻止用户删除 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)
Run Code Online (Sandbox Code Playgroud)

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

另请注意,我没有覆盖我的计费模型的delete功能。

Ole*_*kin 8

另一种选择是覆盖主 ModelAdmin 中专门为此方法设计的get_deleted_objectsCompany - 以允许在从管理 Web 删除公司时删除所有相关对象。

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
Run Code Online (Sandbox Code Playgroud)

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


通过 django admin 删除对象时:

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

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

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


tre*_*zko 4

经过一番挖掘后,看起来确实ModelAdmin简单地调用delete()该对象,这意味着它不应该专门查看您的管理员计费权限。查看模型删除也证实它不关心管理员权限是什么。

我很好奇,想知道该has_delete_permission函数是否会查看相关对象。事实似乎也并非如此。此时,我很好奇您是否覆盖了Billing模型的delete功能?这会阻止删除,并且如果您已将关系CASCADE设置为您的,则此时on_delete将不允许您完成删除,因为它无法级联删除。Company

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


话虽如此,我不知道我是否同意这种做法。我认为在 的模型级别强制执行这一点会更有意义Billing。尝试 a 时delete,您可以检查 a 是否没有其他Billing对象Company,如果有,则引发验证错误,通知用户 aCompany必须至少有一个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()
Run Code Online (Sandbox Code Playgroud)

编辑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)
Run Code Online (Sandbox Code Playgroud)

编辑:我确实发现您可以访问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
Run Code Online (Sandbox Code Playgroud)