Show all the fields in inline of the many-to-many model instead of a simple dropdown

Hello,

I never had a need to customize the django admin. But the time has come.

The scenario is - user comes to a deal change page. He can then add loans through the inline. He also should be able to add vessels to the current deal through the inline. It should be possible to add more than one vessel. The same vessel should be possible to add into another deal as well(that’s why there is m2m relationship). User currently can add loans, he can also add vessels, but information of the added vessel is not displayed(as it is in the loan inline), only a dropdown is shown.

The best would be to have the first column: dropdown of the vessels(vessel_code), second column - ship_type, third column - ship_sub_type. If the user picks vessel from the dropdown - the second and the third columns fill with the chosen vessels values.

I have a Vessel model:

from django.db import models

class Vessel(models.Model):
    vessel_code = models.CharField(max_length=64)
    ship_type = models.CharField(
        max_length=32,
        blank=True,
        null=True,
    )
    ship_sub_type = models.CharField(
        max_length=64,
        blank=True,
        null=True,
    )
    
    def __str__(self):
        return f"{self.vessel_code}"

I have a Loan model:

from django.db import models
from deal.models import Deal


class Loan(models.Model):
    loan_name = models.CharField(max_length=128)
    loan_subname = models.CharField(max_length=128)
    loan_amount = models.DecimalField(
        max_digits=19, decimal_places=6, blank=True, null=True
    )
    deal = models.ForeignKey(Deal, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.loan_name}"

And finally I have Deal model:

from django.db import models
from vessel.models import Vessel

class Deal(models.Model):
    name = models.CharField(max_length=128)
    vessels = models.ManyToManyField(Vessel, through='IntermediateModel')

    def __str__(self):
        return f"{self.name}"

class IntermediateModel(models.Model):
    deal = models.ForeignKey(Deal, on_delete=models.CASCADE)
    vessel = models.ForeignKey(Vessel, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.deal.name} - {self.vessel.vessel_code}"

What I need to achieve is to be able to modify/add/delete loans and vessels from the Deal model admin.

Inside of the deal/admin.py I have registered some inlines as such:

from django.contrib import admin
from loan.models import Loan
from deal.models import Deal, IntermediateModel
from vessel.models import Vessel


class VesselInline(admin.TabularInline):
    model = IntermediateModel
    extra = 1
    

class LoanInline(admin.TabularInline):
    model = Loan
    extra = 0


class NewAdmin(admin.ModelAdmin):
    inlines = [
        LoanInline, 
        VesselInline,
    ]

admin.site.register(Deal, NewAdmin)

How the deal change page looks now:

As we can see I can both add loans and vessels from within Deal model. That’s good, that’s what I wanted.

The problem is HOW the vessel objects are displayed. Currently its a simple dropdown. I can not see the vessel model fields (“vessel_code”, “ship_type”, “ship_sub_type”). And also I am not able to edit those fields from within the deal model(there is a pop-up, yes, but that is not what the need is). I would need a similar functionality as the Loan inline above.

Is it possible to show ALL the fields of the many-to-many inline instead of a simple dropdown? Similar in the loan model inline?

If there is a one-to-one relationship between deal and vessel - I have no problem, it works just as I want.

Has anyone tried something like this? Is there some built in functionality I overlooked in the docs? Should I create some custom forms? Or modify the Django admin in some other ways?

Any help or workarounds are very much appreciated!

See the section of the Admin docs for Working with many-to-many models. and the section after that on Working with many-to-many intermediary models to see if that will produce what you want.

Thank you for the response, Ken!

I have looked through the doc sections that you have referenced, if we are looking into this - The Django admin site | Django documentation | Django , then we can see that my models are build very similarly to this example:

Person → Vessel
Groups → Deal
Membership → IntermediateModel

And my admin looks like the picture I have added above, not really close to what I need.

  1. Maybe I should try to create a custom form with all the needed fields(vessel_code(autocomplete), ship_type, ship_sub_type.) and then squeeze this form into Deal add/change page…? Then upon save I would do the check if the added vessel(from the autocomplete) is already in the DB, if yes - fetch it’s details in the created ship_type, ship_sub_type fields. If its new - register the vessel_code into the DB as a new vessel.

  2. Modify the default inline template to add custom fields to the right of the dropdown? Using custom template tags perhaps…?

  3. Ditch django admin and build a custom UI with class based views, listpages, detailpages and custom forms and hope that this is easier achievable than modifying the admin…? I would maybe have more freedom building custom views/templates than trying to get the needed functionality using prebuilt django admin?

Ken, What would you do in this case? :slight_smile:

p.s. I see a lot of user issues that are solved by you on this django forum, so I am happy to personally get your assistance! I know we will solve it!

What you’ve got in your original post doesn’t quite match what’s described in that section of the docs referring to using the InlineModelAdmin for the relationship.

Your VesselInline would be defined with model = Deal.vessels.through.

Your NewAdmin (the admin class for Deal), then needs to exclude the vessels field.

I believe my example matches this part of the docs - The Django admin site | Django documentation | Django

And what you are talking about is this - The Django admin site | Django documentation | Django . I tried to make my case close to this one:

Deal Model:

from django.db import models
from vessel.models import Vessel

class Deal(models.Model):
    name = models.CharField(max_length=128)
    vessels = models.ManyToManyField(Vessel)

    def __str__(self):
        return f"{self.name}"

Deal admin:

from django.contrib import admin
from loan.models import Loan
from deal.models import Deal
from vessel.models import Vessel


class VesselInline(admin.TabularInline):
    model = Deal.vessels.through
    extra = 1
    

class LoanInline(admin.TabularInline):
    model = Loan
    extra = 0


class NewAdmin(admin.ModelAdmin):
    inlines = [
        VesselInline,
        LoanInline,
    ]

admin.site.register(Deal, NewAdmin)

The admin looks like such then:

If I add exclude = ["vessels"] to NewAdmin, then the Vessels field (second from the top) dissapears, because yes, it is redundant, since we have “DEAL-VESSEL RELATIONSHIPS” section now, which is basically the same.

Still the dropdown only in the “DEAL-VESSEL RELATIONSHIPS” section :confused:

Yep, you’re right - I was confusing two different situations. What I was referring to addresses the need to update the through model, not the target of the many-to-many.

In a way, this actually “fits” with the way that the rest of the admin works.

Think of any foreign key situation, perhaps a “Profile” that has a foreign key to “User”. The admin doesn’t have a facility for you to edit the User from the Profile admin page.

This analogy fits, because while you have the reverse foreign key relationship from Deal to IntermediateModel, what you then have is a forward foreign key relationship from IntermediateModel to Vessel, and that’s the link where the admin doesn’t provide intrinsic support.

Now, having said that, you probably could build something for this by overriding the InlineModelAdmin.form (or the .formset?) attribute, to create your own form containing all the desired fields.

Okay, so one option would be to override the InlineModelAdmin.form.

Before I try that(I’m not bursting with enthusiasm for this task, not an expert of OOP by any means :smiley: ), I have found out that:

when you define a method within an inline class of a ModelAdmin, Django automatically treats it as a callable field. This means that Django will call this method for each instance in the queryset and display the returned value as a column in the inline.

So in that case if I create a few methods ship_type and ship_sub_type and add them to get_readonly_fields like so:

class VesselInline(admin.TabularInline):
    model = Deal.vessels.through
    extra = 0
    
    def ship_type(self, instance):
        return instance.vessel.ship_type if instance.vessel else "-"
    
    def ship_sub_type(self, instance):
        return instance.vessel.ship_sub_type if instance.vessel else "-"
        
    ship_type.short_description = "Ship type"
    ship_sub_type.short_description = "Ship sub type"

    def get_readonly_fields(self, request, obj=None):
        return list(super().get_readonly_fields(request, obj)) + ["ship_type"] + ["ship_sub_type"]

They get rendered like so:

For reference how Vessel 1 looks:
image

That’s a good start! The preferred requirement is for these fields to be editable, but for now I don’t really know how to make then editable. If I use get_fields instead of get_readonly_fields, then Django complains that

Unknown field(s) (ship_type, ship_sub_type) specified for Deal_vessels

Makes sense, it can not find those fields in Deal_vessels(which is a through table), but then how did it find the values and displayed them as readonly :thinking:

What I also did then is:

def has_change_permission(self, request, obj):
        return False

So the edit/add/view buttons dissapear from the inline, to make it a bit more clean for my use-case. So then it looks like this:

TODO:

  1. Now I have to think how to make those fields editable without completely rewriting the form.

  2. And have to figure out how to make it more convenient for the user to choose the vessel. Now it’s a dropdown. But what if I have +40k of vessels registered in the DB? In that case currently it would take ages to load them and also it’s impossible to pick and choose the needed vessel from the current dropdown.

If you have any ideas - looking forward to hearing them!

You can’t. That’s a documented limitation.

Personally, I think your best bet here is to do this as a separate view instead of trying to make this work in the admin.

See Select2 and Django-Select2

Implemented django-autocomplete-light like such:

views.py:

class VesselAutocomplete(autocomplete.Select2QuerySetView):
    def get_queryset(self):
        user = self.request.user
        if not user.is_authenticated:
            return Vessel.objects.none()

        queryset = Vessel.objects.all()

        if self.q:
            queryset = queryset.filter(vessel_code__istartswith=self.q)

        return queryset

app/urls.py

urlpatterns = [
    path(
        "vessel-autocomplete/",
        VesselAutocomplete.as_view(),
        name="vessel-autocomplete",
    ),
]

project/urls.py

    path("vessel/", include("vessel.urls")),

Then rewrote get_formset method in VesselInline like such:

admin.py

class VesselInline(admin.TabularInline):

    model = Deal.vessels.through
    extra = 0

    def get_formset(self, request, obj=None, **kwargs):
        formset = super().get_formset(request, obj, **kwargs)
        formset.form.base_fields['vessel'].widget = autocomplete.ModelSelect2(
            url='vessel-autocomplete',
            attrs={
                'data-placeholder': 'Search for a vessel', 
                'data-minimum-input-length': 2
                }
        )
        return formset

and now instead of a dropdown I have this autocomplete search field:

Which does the job for now.

Thank you for your help @KenWhitesell!