djangodjango-modelsdjango-formsdjango-admindjango-admin-filters

Django Admin dependent DropDown HTML field


I have a Region and SubRegion ForeignKey in the Country Model. My SubRegion model also has a Region ForeignKey for the Region Model. I am having a hard time displaying the SubRegion dropdown in the Country Model on the basis of the Region selected.

What would be the best way to achieve it? It would be great if dropdowns were based on the value of their parent.

models.py

class Region(models.Model):
    name = models.CharField(max_length=20)
    is_active = models.BooleanField(verbose_name='is active?', default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

class SubRegion(models.Model):
    region = models.ForeignKey(Region, on_delete=models.CASCADE)
    name = models.CharField(max_length=50)
    is_active = models.BooleanField(verbose_name='is active?', default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = 'Sub Regions'

class Country(models.Model):
    name = models.CharField(max_length=100)
    region = models.ForeignKey(Region, on_delete=models.CASCADE, verbose_name='Region')
    subregion = models.ForeignKey(SubRegion, on_delete=models.CASCADE, verbose_name='Sub Region')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

admin.py

class CountryAdmin(admin.ModelAdmin):
    list_display = ('name', 'is_active', 'is_default', 'created_at', 'updated_at', )
    list_filter = ('region', 'subregion', 'is_active', 'is_default', 'created_at', 'updated_at', )
    search_fields = ('name', 'official_name', )
    fieldsets = (
        ('Region Information', {'fields': ('region', 'subregion', )}),
        ('Basic Information', {'fields': ('name', 'official_name', )}),
        ('Codes Information', {'fields': ('cca2', 'ccn3', 'cca3', 'cioc','idd', 'status', )}),
        ('Coordinates', {'fields': ('latitude', 'longitude', )}),
        ('Membership & Statuses', {'fields': (('is_independent', 'is_un_member', 'is_default', 'is_active', ), )}),
        ('Country Flag', {'fields': ('png_flag_url', 'svg_flag_url', )}),
        ('Country Map', {'fields': ('google_map_url', 'openstreet_map_url', )}),
        ('Additional Information', {'fields': ('population', 'area', 'start_of_week', 'postal_code_format', 'postal_code_regex', )}),
    )
    inlines = [CountryTopLevelDomainInline, CountryCurrencyInline, CountryTimezoneInline]


admin.site.register(Country, CountryAdmin)

Currently, Whenever I am going to add/edit a country then the complete region and subregion are visible on the select dropdown. What I want like possibilities to load the subregion on the basis of region selection.


Solution

  • After long research, I was able to solve this issue.

    I have created the JS file inside my application.

    static\geo_location\js\chained-region.js

    var response_cache = {};
    function get_subregion_for_region(region_id) {
        (function($){
            var selected_value = $("#id_subregion").val();
            if(response_cache[region_id]){
                $("#id_subregion").html(response_cache[region_id]);
                $("select#id_subregion option").each(function(){
                    if ($(this).val() == selected_value)
                        $(this).attr("selected","selected");
                });
            } else {
                $.getJSON("/geo-location/get-subregions-for-region", {region_id:region_id}, function(res, textStatus){
                    var options = '<option value="" selected="selected">---------</option>';
                    $.each(res.data, function(i, item){
                        options += '<option value="' + item.id + '">' + item.name + '</option>';
                    });
                    response_cache[region_id] = options;
                    $("#id_subregion").html(response_cache[region_id]);
                    $("select#id_subregion option").each(function(){
                        if ($(this).val() == selected_value)
                            $(this).attr("selected","selected");
                    });
                });
            }
        })(django.jQuery);
    }
    

    then, added the views function to handle AJAX requests.

    views.py

    from django.http import JsonResponse
    from django.contrib.auth.decorators import login_required
    from .models import SubRegion
    
    
    @login_required
    def subregion_for_region(request):
        if 'region_id' in request.GET and request.GET['region_id'] != '':
            subregions = SubRegion.objects.filter(region=request.GET['region_id'])
            return JsonResponse({'data': [{'id': subregion.id, 'name': subregion.name} for subregion in subregions]})
        else:
            return JsonResponse({'data': []})
    

    Note:- I have added the @login_required decorator to prevent the unnecessary call.

    And, in the admin.py I have created the form and added the "onchange" event on the region dropdown.

    admin.py

    from django import forms
    class CountryForm(forms.ModelForm):
        class Meta:
            model = Country
            fields = '__all__'
            widgets = {
                'region': forms.Select(attrs={'onchange': 'get_subregion_for_region(this.value);'})
            }
    
    class CountryAdmin(admin.ModelAdmin):
        ....
    
        form = CountryForm
    
        class Media:
            js = (
                'geo_location/js/chained-region.js',
            )
    

    And last added the URL pattern in the application urls.py file. urls.py

    from django.urls import path
    from . import views
    
    app_name = 'geo_location'
    urlpatterns = [
        path('get-subregions-for-region/', views.subregion_for_region, name='subregions-for-region'),
    ]