From d4df21c6f89240f30254571247e15b6786217c3c Mon Sep 17 00:00:00 2001 From: Justin Gregory Date: Fri, 11 Jul 2025 17:50:26 -0500 Subject: [PATCH] Allow use of can_delete flag directly in inline definition The previous README had instructions about preventing the deletion of inlined models by overriding the `has_delete_permission` method on a custom subclass of the appropriate `StackedInline` or `TabularInline` class, and then passing that subclass as the `admin_class` option of `inline_reverse`. However, that method no longer works with newer versions of Django (at least with Django 4.2 and later). This commit updates the `ReverseInlineModelAdmin` class to determine `can_delete` in the same way that Django's `InlineModelAdmin` currently does, and then passes that into the custom formset factory. In that way, consumers of this library can simply set the `can_delete` option directly on their `inline_reverse` specification without subclassing one of the built-in `InlineModelAdmin` implementations. Note that this approach was necessary because the custom formset factory in `django_reverse_admin` lives outside of the `ReverseInlineModelAdmin` class, and doesn't have access to the parent class' `can_delete` property and `has_delete_permission` method. --- README.md | 16 ++-------------- django_reverse_admin/__init__.py | 10 +++++++--- tests/polls/admin.py | 11 +++++++++++ tests/polls/models.py | 4 ++++ 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index de5fa46..6280639 100644 --- a/README.md +++ b/README.md @@ -105,21 +105,9 @@ class PersonAdmin(ReverseModelAdmin): #### Do not allow deletion -django-reverse-admin uses the standard features of in-line forms (Stacked and Tabular). When used in the ReverseAdmin co-text, it releases the option to delete the associated object. - -To remove this option (thus making the object unable to be deleted), it is necessary to add the admin_class inheriting from one of the native resources (Stacked and Tabular). Example: +To prevent deletion of the inlined object, use the `can_delete` option: ```py -from .forms import CustomBusinessAddrForm - - -class BarInline(admin.StackedInline): - model = Address - - def has_delete_permission(self, request, obj=None): - return False - - class PersonAdmin(ReverseModelAdmin): inline_type = 'tabular' inline_reverse = [ @@ -127,7 +115,7 @@ class PersonAdmin(ReverseModelAdmin): { 'field_name': 'home_addr', 'fields': ('zip_code',), - 'admin_class': BarInline + 'can_delete': False, } ] ``` diff --git a/django_reverse_admin/__init__.py b/django_reverse_admin/__init__.py index 63961be..5395a30 100644 --- a/django_reverse_admin/__init__.py +++ b/django_reverse_admin/__init__.py @@ -65,14 +65,15 @@ def reverse_inlineformset_factory(parent_model, form=ModelForm, fields=None, exclude=None, - formfield_callback=lambda f: f.formfield()): + formfield_callback=lambda f: f.formfield(), + **kwargs): if fields is None and exclude is None: related_fields = [f for f in model._meta.get_fields() if (f.one_to_many or f.one_to_one or f.many_to_many) and f.auto_created and not f.concrete] fields = [f.name for f in model._meta.get_fields() if f not in related_fields] # ignoring reverse relations - kwargs = { + defaults = { 'form': form, 'formfield_callback': formfield_callback, 'formset': ReverseInlineFormSet, @@ -83,7 +84,8 @@ def reverse_inlineformset_factory(parent_model, 'exclude': exclude, 'max_num': 1, } - FormSet = modelformset_factory(model, **kwargs) + defaults.update(**kwargs) + FormSet = modelformset_factory(model, **defaults) FormSet.parent_fk_name = parent_fk_name return FormSet @@ -129,12 +131,14 @@ def get_formset(self, request, obj=None, **kwargs): exclude.extend(non_editable_fields) # but need exclude to be None if result is an empty list exclude = exclude or None + can_delete = self.can_delete and self.has_delete_permission(request, obj) defaults = { "form": self.form, "fields": fields, "exclude": exclude, "formfield_callback": partial(self.formfield_for_dbfield, request=request), + "can_delete": can_delete, } kwargs.update(defaults) return reverse_inlineformset_factory(self.parent_model, diff --git a/tests/polls/admin.py b/tests/polls/admin.py index dc17daa..b677824 100644 --- a/tests/polls/admin.py +++ b/tests/polls/admin.py @@ -2,6 +2,7 @@ from polls.models import Address from polls.models import NonInlinePerson from polls.models import Person +from polls.models import PersonNoDeleteAddress from polls.models import PersonWithAddressNonId from polls.models import PersonWithTwoAddresses from polls.models import PhoneNumber @@ -41,6 +42,15 @@ def get_readonly_fields(self, request, obj=None): return self.readonly_fields +class PersonNoDeleteAddressAdmin(PersonAdmin): + inline_reverse = [ + ('home_addr', { + 'fields': ['street', 'city', 'state', 'zipcode'], + 'can_delete': False, + }), + ] + + class PersonWithAddressNonIdAdmin(ReverseModelAdmin): list_display = ('name', 'home_addr') @@ -74,6 +84,7 @@ class AddressAdmin(admin.ModelAdmin): admin.site.register(Person, PersonAdmin) +admin.site.register(PersonNoDeleteAddress, PersonNoDeleteAddressAdmin) admin.site.register(PersonWithAddressNonId, PersonWithAddressNonIdAdmin) admin.site.register(PersonWithTwoAddresses, PersonWithTwoAddressesAdmin) admin.site.register(PhoneNumber, PhoneNumberAdmin) diff --git a/tests/polls/models.py b/tests/polls/models.py index a6d1006..53303b1 100644 --- a/tests/polls/models.py +++ b/tests/polls/models.py @@ -56,6 +56,10 @@ def __str__(self): return self.name +class PersonNoDeleteAddress(Person): + pass + + class PersonWithAddressNonId(TemporalBase): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=255)