23

How can I access the parent instance from the inline model admin?

My goal is to override the has_add_permission function based on the status of the parent instance. I don't want to allow to add a child if the status of the parent is different than 1.

class ChildInline(admin.TabularInline):
    model = Child
    form = ChildForm

    fields = (
        ...
    )
    extra = 0

    def has_add_permission(self, request):
        # Return True only if the parent has status == 1
        # How to get to the parent instance?
        #return True

class ParentAdmin(admin.ModelAdmin):
    inlines = [ChildInline,]
Michael
  • 8,357
  • 20
  • 58
  • 86

7 Answers7

31

Django < 2.0 Answer:

Use Django's Request object (which you have access to) to retrieve the request.path_info, then retrieve the PK from the args in the resolve match. Example:

from django.contrib import admin
from django.core.urlresolvers import resolve
from app.models import YourParentModel, YourInlineModel


class YourInlineModelInline(admin.StackedInline):
    model = YourInlineModel

    def get_parent_object_from_request(self, request):
        """
        Returns the parent object from the request or None.

        Note that this only works for Inlines, because the `parent_model`
        is not available in the regular admin.ModelAdmin as an attribute.
        """
        resolved = resolve(request.path_info)
        if resolved.args:
            return self.parent_model.objects.get(pk=resolved.args[0])
        return None

    def has_add_permission(self, request):
        parent = self.get_parent_object_from_request(request)

        # Validate that the parent status is active (1)
        if parent:
            return parent.status == 1

        # No parent - return original has_add_permission() check
        return super(YourInlineModelInline, self).has_add_permission(request)


@admin.register(YourParentModel)
class YourParentModelAdmin(admin.ModelAdmin):
    inlines = [YourInlineModelInline]

Django >= 2.0 Answer:

Credit to Mark Chackerian for the below update:

Use Django's Request object (which you have access to) to retrieve the request.path_info, then retrieve the PK from the args in the resolve match. Example:

from django.contrib import admin
from django.urls import resolve
from app.models import YourParentModel, YourInlineModel


class YourInlineModelInline(admin.StackedInline):
    model = YourInlineModel

    def get_parent_object_from_request(self, request):
        """
        Returns the parent object from the request or None.

        Note that this only works for Inlines, because the `parent_model`
        is not available in the regular admin.ModelAdmin as an attribute.
        """
        resolved = resolve(request.path_info)
        if resolved.args:
            return self.parent_model.objects.get(pk=resolved.args[0])
        return None

    def has_add_permission(self, request):
        parent = self.get_parent_object_from_request(request)

        # Validate that the parent status is active (1)
        if parent:
            return parent.status == 1

        # No parent - return original has_add_permission() check
        return super(YourInlineModelInline, self).has_add_permission(request)


@admin.register(YourParentModel)
class YourParentModelAdmin(admin.ModelAdmin):
    inlines = [YourInlineModelInline]
Michael B
  • 5,148
  • 1
  • 28
  • 32
  • 1
    this is the only way it works. Strange that it is sooo hard to get access to parent object. It took me 1 hour to find this solution. – zaphod100.10 Jul 24 '17 at 08:24
  • 12
    I had change a few things for python 3. 1) resolve import is `from django.urls import resolve` 2) "resolved" logic is `if resolved.kwargs.get('object_id'): return self.parent_model.objects.get(pk=resolved.kwargs['object_id'])` . – Mark Chackerian Mar 23 '18 at 17:28
  • I wasn't able to find a `self.parent_model` attribute in 2.2.1, but `__init__` has `kwargs['instance']` for me which seems to contain the parent model instance. – bparker May 31 '19 at 21:20
  • No need to `import resolve`: a `request.resolver_match` attribute is already available, so you can use `request.resolver_match.kwargs.get('object_id')`. This holds for Django 2.2, and also for 3.1 ([source](https://github.com/django/django/blob/stable/3.1.x/django/core/handlers/base.py#L289)). – djvg Oct 06 '20 at 12:13
  • @MarkChackerian Is there some way of getting the inline model instance in a similar manner? This method will get us the instance of `Parent` model associated with `ParentAdmin`. How can we get the instance of `Child` model? – musical_ant Feb 19 '22 at 14:56
23

I think this is a cleaner way to get the parent instance in the inline model.

class ChildInline(admin.TabularInline):
    model = Child
    form = ChildForm

    fields = (...)
    extra = 0

    def get_formset(self, request, obj=None, **kwargs):
        self.parent_obj = obj
        return super(ChildInline, self).get_formset(request, obj, **kwargs)

    def has_add_permission(self, request):
        # Return True only if the parent has status == 1
        return self.parent_obj.status == 1


class ParentAdmin(admin.ModelAdmin):
    inlines = [ChildInline, ]
Richard Cotrina
  • 2,525
  • 1
  • 23
  • 23
  • I cannot get this to work as-is. I'm getting "Parent_obj" is not an attribute of ChildInline. – Richard Cooke Apr 27 '17 at 15:19
  • 1
    Thats weird, It works for me... Which version of Django are you using? – Richard Cotrina Apr 28 '17 at 01:06
  • Django 1.10 under Python 2.7 – Richard Cooke May 01 '17 at 17:12
  • Nope. I dumped out a DIR of the object, and as Python said, no such member. So I implemented the messier solution above. This was a few projects ago, not even sure which project this is in. New projects are in Django 2 and Python 3. Time will tell if this comes up again. And I will try your much cleaner solution first! – Richard Cooke Feb 13 '18 at 14:02
  • For what it is worth, `has_add_permission` gets called prior to `get_formset`. The problem with this solution (I tested on Django 1.8) is the order in which the code is executed. Prior to generating the forms, it checks to see if you are allowed to add them. – Michael B Sep 28 '18 at 16:40
5

I tried the solution of Michael B but didn't work for me, I had to use this instead (a small modification that uses kwargs):

def get_parent_object_from_request(self, request):
        """
        Returns the parent object from the request or None.

        Note that this only works for Inlines, because the `parent_model`
        is not available in the regular admin.ModelAdmin as an attribute.
        """
        resolved = resolve(request.path_info)
        if resolved.kwargs:
            return self.parent_model.objects.get(pk=resolved.kwargs["object_id"])
        return None
jorge4larcon
  • 81
  • 1
  • 2
1

BaseInlineFormSet has the attribute self.instance which is the reference to the parent object.

In the constructor, the queryset is intialized and filtered using this instance. If you need adjustments, you can change the queryset argument to the constructor or use inlineformset_factory to set the formset up according to your needs.

class BaseInlineFormSet(BaseModelFormSet):
    """A formset for child objects related to a parent."""
    def __init__(self, data=None, files=None, instance=None,
                 save_as_new=False, prefix=None, queryset=None, **kwargs):
        if instance is None:
            self.instance = self.fk.remote_field.model()
        else:
            self.instance = instance
        self.save_as_new = save_as_new
        if queryset is None:
            queryset = self.model._default_manager
        if self.instance.pk is not None:
            qs = queryset.filter(**{self.fk.name: self.instance})
        else:
            qs = queryset.none()
        self.unique_fields = {self.fk.name}
        super().__init__(data, files, prefix=prefix, queryset=qs, **kwargs)

see https://docs.djangoproject.com/en/3.2/_modules/django/forms/models/

If you extends from this, make sure to run super().__init__() before accessing self.instance.

Risadinha
  • 16,058
  • 2
  • 88
  • 91
  • I tried calling `self.instance` and I got an `*** AttributeError: 'MyInline' object has no attribute 'instance'` - that doesn't seem to work – Patrick Jul 26 '22 at 11:42
  • @Patrick - make sure to call `super().__init__()`, see my edit. – Risadinha Jul 29 '22 at 07:54
1

Place the following on the parent admin model so that the parent model instance is available to any inline under the parent model:

def get_form(self, request, obj=None, **kwargs):
    request._obj = obj
    return super().get_form(request, obj, **kwargs)

Then, in the inline (using a customers field as an example):

def formfield_for_manytomany(self, db_field, request, **kwargs):
    if db_field.name == "customers":
        if request._obj is not None:
            kwargs["queryset"] = request._obj.customers
        else:
            kwargs["queryset"] = Customer.objects.none()            
    return super().formfield_for_manytomany(db_field, request, **kwargs)
Dan Swain
  • 2,910
  • 1
  • 16
  • 36
0

You only need to add obj parameter and check the parent model status

class ChildInline(admin.TabularInline):
   model = Child
   form = ChildForm

   fields = (
    ...
    )
   extra = 0
   #You only need to add obj parameter 
   #obj is parent object now you can easily check parent object status
   def has_add_permission(self, request, obj=None):
        if obj.status == 1:
           return True
        else:
           return False


   class ParentAdmin(admin.ModelAdmin):
         inlines = [ChildInline,]
0

You can also retrieve it simply from the request path using re module if you do not expect numbers in your path.

for example:

import re

def get_queryset(self, request):
    instance_id = re.sub(r"\D+", "", request.path)

or

def get_parent_object_from_request(self, request):
    instance_id = re.sub(r"\D+", "", request.path)
David Louda
  • 498
  • 5
  • 19