10

Given a model named MainModel and a RelatedModel, where the later has a ForeignKey field to MainModel:

class MainModel(models.Model):
    name = models.CharField(max_length=50)
    type = models.BooleanField()

class RelatedModel1(models.Model):
    main = models.ForeingKey(MainModel):
    name = models.CharField(max_length=50)

class RelatedModel2(models.Model):
    main = models.ForeingKey(MainModel):
    name = models.CharField(max_length=50)

and the corresponding ModelAdmin classes:

class RelatedModel1InlineAdmin(admin.TabularInline):
    model = RelatedModel1

class RelatedModel2InlineAdmin(admin.TabularInline):
    model = RelatedModel2

class MainModel(admin.ModelAdmin):
    inlines = [RelatedModel1, RelatedModel2]

And that's the default behavior, you get two inlines, one for every related model. The question is how to hide completely all the inlines when the MainModel instance is being created (the ModelAdmin's add_view), and to show the inlines for RelatedModel1 when the type field of the MainModel instance is True, and show the inlines for RelatedModel2 when False.

I was going to create a descriptor for the ModelAdmin.inline_instances attribute, but I realized that I need access to the object instance being edited, but it is passed around as parameters.

Any help?

Thanks!

Armando Pérez Marqués
  • 5,661
  • 4
  • 28
  • 45

6 Answers6

4

@Yuji 'Tomita' Tomitayou the idea was good, i had the same but once trying, i realized you must also remove specific key from self.inlines because in change_view and add_view method self.get_inline_instances(request) is called before get_formsets(). Therefore i moved inlines handling to get_form() method.

Here is how i sucessfully did it:

class SampleAdmin(ModelAdmin):
    inlines = []

    def get_inlines(self):
        return [SampleInline, SampleInline2]

    def get_form(self, request, obj=None, **kwargs):
        # due to django admin form fields caching you must 
        # redefine inlines on every `get_form()` call
        if (obj): self.inlines = self.get_inlines()
        for inline in self.inlines:
            # Here change condition based on your needs and manipulate
            # self.inlines as you like (remove, change, etc). 
            # I used inline.__name__ to detect if this is correct inline 
            # for my obj
            if obj.CONDITION:
                self.inlines.remove(inline)
        return super(SampleAdmin, self).get_form(request, obj, **kwargs)
darklow
  • 2,249
  • 24
  • 22
3

I realize this question's a bit old and the codebase has changed a bit; there's a cleanish point to override things at now: get_inline_instances. You can do this:

class MainModelAdmin(models.ModelAdmin):
    inlines = [RelatedModel1InlineAdmin,RelatedModel2InlineAdmin]

    def get_inline_instances(self, request, obj=None):
        #Return no inlines when obj is being created
        if not obj:
            return []
        unfiltered = super(MainModelAdmin, self).get_inline_instances(request, obj)
        #filter out the Inlines you don't want
        if obj.type:
            return [x for x in unfiltered if isinstance(x,RelatedModel1InlineAdmin)]
        else:
            return [x for x in unfiltered if isinstance(x,RelatedModel2InlineAdmin)]
aggieNick02
  • 2,557
  • 2
  • 23
  • 36
2

This worked for me while searching for an answer to the same problem in this old post. Expanding upon darklow's answer , I think you can simply override get_inline_instances completely and add an extra check based on your type.

  1. Add a boolean type check method in your model

    class MainModel(models.Model):
    
        name = models.CharField(max_length=50)
    
        type = models.BooleanField()
    
        def is_type1(self):
    
           return type=="some value"
    
        def is_type2(self):
            return type=="some value"
    
  2. Add inline instance base on type check - Simply copy and paste the get_inline_insances method from the parent class into your admin.ModelAdmin class and add the if block to check the model type as shown below

    class MyModelAdmin(admin.ModelAdmin):
    
        inlines = [RelatedModel1, RelatedModel2]
    
        def get_inline_instances(self, request, obj=None):
            inline_instances = []
            if not obj:
                return []
            for inline_class in self.inlines:
                inline = inline_class(self.model, self.admin_site)
                if request:
                    if not (inline.has_add_permission(request) or
                                inline.has_change_permission(request, obj) or
                                inline.has_delete_permission(request, obj)):
                        continue
                    if not inline.has_add_permission(request):
                        inline.max_num = 0
                if obj.is_type1() and isinstance(inline,RelatedModel1InlineAdmin):
                    inline_instances.append(inline)
                if obj.is_type2() and isinstance(inline,RelatedModel2InlineAdmin):
                    inline_instances.append(inline)
    
            return inline_instances
    
Mark Chackerian
  • 21,866
  • 6
  • 108
  • 99
JHRS
  • 367
  • 4
  • 11
2

You need just simply override change_view in ModelAdmin:

def change_view(self, request, object_id, form_url='', extra_context=None):
    obj = self.model.objects.filter(pk=object_id).first()
    if not obj:
        self.inlines = []
    else:
        if obj.type is True:
            self.inlines = [RelatedModel1InlineAdmin]
        else:
            self.inlines = [RelatedModel2InlineAdmin]

    return super().change_view(request,object_id,form_url=form_url,extra_context=extra_context)

that's work for me.

Allen Shaw
  • 1,164
  • 7
  • 23
1

From peeking at contrib.admin.options.pyLooks like you could override ModelAdmin.get_formsets. Note that the admin site populates self.inline_instances at __init__, so you probably want to follow and not instantiate your inlines over and over. I'm not sure how expensive it is : )

def get_formsets(self, request, obj=None):
    if not obj:
        return [] # no inlines

    elif obj.type == True:
        return [MyInline1(self.model, self.admin_site).get_formset(request, obj)]

    elif obj.type == False:
        return [MyInline2(self.model, self.admin_site).get_formset(request, obj)]

    # again, not sure how expensive MyInline(self.model, self.admin_site) is. 
    # the admin does this once. You could instantiate them and store them on the 
    # admin class somewhere to reference instead.

The original admin get_formsets uses generators - you could too to more closely mimic the original:

def get_formsets(self, request, obj=None):
    for inline in self.inline_instances:
        yield inline.get_formset(request, obj)
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • Turns out this doesn't work, because this in the ModelAdmin's `change_view`/`add_view`: `for FormSet, inline in zip(self.get_formsets(request, obj), self.inline_instances):` Thus, a list with one formset is returned, but when it gets "zipped" with a list of inlines, not always the formset returned corresponds to the first inline in the list. – Armando Pérez Marqués Nov 14 '11 at 04:12
  • Ouch, you're right! I'm not sure I see a good way around that self.inline_instances call. A lock to render a view at a time? :( Dang. I was actually pretty excited about this idea because I've run into it myself. – Yuji 'Tomita' Tomita Nov 14 '11 at 21:32
  • What about this: `get_formsets` should return Formset instances, so, what about "disabling" those that aren't related to the value of `obj.type`? – Armando Pérez Marqués Nov 14 '11 at 22:03
  • @Mandx interesting... how would you disable it? Setting up a dummy FormSet that accepts any model and overrides the queryset to return nothing would work I think. But at this point in hackery I would suggest disabling them through JavaScript (hiding them) with ModelAdmins support for custom JS scripts. – Yuji 'Tomita' Tomita Nov 15 '11 at 01:26
0

Here is a piece of code I wrote when I was faced with the same problem. It is a bit brute force style, I guess, but is very agile and should suit all cases.

class MyModelAdmin(admin.ModelAdmin):
    def __init__(self, *args, **kwargs):
        super(MyModelAdmin, self).__init__(*args, **kwargs)
        self.inline_instances_hash = {}
        for inline_class in self.inlines:
            for inline_instance in self.inline_instances:
                if isinstance(inline_instance, inline_class):
                    break
            self.inline_instances_hash[inline_class] = inline_instance

    def get_inline_instance(self, inline_class):
        return self.inline_instances_hash[inline_class]

    def get_form(self, request, obj=None, **kwargs):
        if obj:
            self.inline_instances = []
            if self.CONDITION:
                self.inline_instances.append(self.get_inline_instance(
                    THE_INLINE_CLASS_I_WANT))
            #...
        else:
            self.inline_instances = self.inline_instances_hash.values()
Lyudmil Nenov
  • 247
  • 2
  • 6