Django Admin - how to prevent deletion of some of

2020-06-25 04:29发布

问题:

I have 2 models - for example, Book and Page. Page has a foreign key to Book.

Each page can be marked as "was_read" (boolean), and I want to prevent deleting pages that were read (in the admin).

In the admin - Page is an inline within Book (I don't want Page to be a standalone model in the admin).

My problem - how can I achieve the behavior that a page that was read won't be deleted? I'm using Django 1.4 and I tried several options:

  1. Override "delete" to throw a ValidationError - the problem is that the admin doesn't "catch" the ValidationError on delete and you get an error page, so this is not a good option.
  2. Override in the PageAdminInline the method - has_delete_permission - the problem here -it's per type so either I allow to delete all pages or I don't.

Are there any other good options without overriding the html code?

Thanks, Li

回答1:

The solution is as follows (no HTML code is required):

In admin file, define the following:

from django.forms.models import BaseInlineFormSet

class PageFormSet(BaseInlineFormSet):

    def clean(self):
        super(PageFormSet, self).clean()

        for form in self.forms:
            if not hasattr(form, 'cleaned_data'):
                continue                     

            data = form.cleaned_data
            curr_instance = form.instance
            was_read = curr_instance.was_read


            if (data.get('DELETE') and was_read):            
                raise ValidationError('Error')



class PageInline(admin.TabularInline):
    model = Page
    formset = PageFormSet


回答2:

You could disable the delete checkbox UI-wise by creating your own custom formset for the inline model, and set can_delete to False there. For example:

from django.forms import models 
from django.contrib import admin 

class MyInline(models.BaseInlineFormSet): 
    def __init__(self, *args, **kwargs): 
        super(MyInline, self).__init__(*args, **kwargs) 
        self.can_delete = False 

class InlineOptions(admin.StackedInline): 
    model = InlineModel 
    formset = MyInline 

class MainOptions(admin.ModelAdmin): 
    model = MainModel 
    inlines = [InlineOptions]


回答3:

Another technique is to disable the DELETE checkbox. This solution has the benefit of giving visual feedback to the user because she will see a grayed-out checkbox.

from django.forms.models import BaseInlineFormSet

class MyInlineFormSet(BaseInlineFormSet):

    def add_fields(self, form, index):
        super().add_fields(form, index)
        if some_criteria_to_prevent_deletion:
            form.fields['DELETE'].disabled = True

This code leverages the Field.disabled property added in Django 1.9. As the documentation says, "even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data," so you don't need to add more code to prevent deletion.



回答4:

I found a very easy solution to quietly avoid unwanted deletion of some inlines. You can just override delete_forms property method. This works not just on admin, but on regular inlines too.

from django.forms.models import BaseInlineFormSet

class MyInlineFormSet(BaseInlineFormSet):

    @property
    def deleted_forms(self):
        deleted_forms = super(MyInlineFormSet, self).deleted_forms

        for i, form in enumerate(deleted_forms):
            # Use form.instance to access object instance if needed
            if some_criteria_to_prevent_deletion:
                deleted_forms.pop(i)

        return deleted_forms