Ajax based drop down menu in Django admin

hi,
How can I create admin model form so that when I select one field from a drop down list say select a staton name from a list, all tags belonging to the selected station name get loaded automatically using AJAX?

Thanks,
Anu

Can you be more specific about what you’re looking to do here? In this case, please post the model involved and what you want to have happen with that model.

Sorry I was not clear. Please find the models and requirements below. As we can see from the models below, tags belong to a particular station. I am adding/editing data in these 2 models in a single page (as shown in the screen shot below), through my app’s Django admin. My requirement is when I choose a station name from a drop down, all the tags related to this station should load via AJAX automatically. Currently I am choosing a station name and I click save and continue editing in Django admin and after the page refresh all tags for that station name gets loaded. Waiting for the page refresh to load all tags belonging to a station name is very time consuming when adding details in bulk. Hence looking for ways to make an AJAX call every time the station name changes so that the tags are pulled and send back to the admin form asynchronously.

In Models.py

class Station(FeedmachineModel):
slug = models.SlugField(unique=True)
name = models.TextField()
domain = models.TextField()
timezone = models.TextField(choices=TIMEZONE_CHOICES)

class Meta:
    ordering = ['slug']

def __str__(self):
    return self.slug + ': ' + self.domain

class Tag(CmsObject, FeedmachineModel):
name = models.TextField()
station = models.ForeignKey(‘Station’, on_delete=models.CASCADE)

objects = TagManager()

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

What I believe you would need to do would be to write some JavaScript that gets added to the page (see Form Assets (the Media class) | Django documentation | Django).

This JavaScript would be activated based on the onchange attribute of the dropdown field. It would call a new view that you will create that will return the list of selected elements to be updated. When the view returns that list, it will then update the select widget on the page.

Thanks a lot for the lead, yes I added the media class inside the FeedAdmin and attached a javascript file containing code that make the AJAX call. I have implemented the feature successfully but I see some new bugs have been introduced. I have attached screenshots to explain in detail
Step 1 - Station name = DEVN
tags and video tags belonging to the station name loaded correctly.
Step 2- Change station name to KGUN (IMPLEMENTED AJAX CALL TO LOAD TAGS, VIDEO TAGS FOR THIS NEW STATION)
Step 3 - tags, video tags for new station name KGUN is loaded. Now Select any tag or video tag
Step 4 - click save and continue editing
Step 5 - expectation - save is successful
Step 6 - reality, Django throwing validation errors- it says the tags, video tags selected are invalid. Internally Django is still keeping DEVN in its validation fields and checking tags,stations for DEVN instead of KGUN.
Please help us understand what’s happening.

When you post the form back to the server, Django is trying to bind the form data to the Django form. It’s going to perform validation based upon how the form is defined. By default, it’s not going to know what was selected on the form until after the form has been bound - kind of a catch-22 situation.

What I suggest you do is at the field level, allow anything to be valid in the video tags field, and create a custom clean method on the form to allow you to check that field based upon the contents of the station name field.

Thank you for the quick reply. When you say at the field level, allow anything to be valid in the video tags field," I understand I must disable the Django internal validation. How do I do this? Tags belong to a Station. Video tags belong to Station.
In Models.py

class Feed(FeedmachineModel):
    slug = models.SlugField()
    station = models.ForeignKey(
        'Station',
        on_delete=models.PROTECT,
        related_name='feeds'
    )
    ad_policy = models.ForeignKey(
        'FeedAdPolicy',
        on_delete=models.PROTECT,
        related_name='feeds',
        null=True,
        blank=True
    )
    inject_carousel = models.BooleanField(default=False)

    objects = FeedManager()

    class Meta:
        ordering = ['slug']

class Station(FeedmachineModel):
    TIMEZONE_CHOICES = [
        ('US/Eastern', 'Eastern'),
        ('US/Central', 'Central'),
        ('US/Mountain', 'Mountain'),
        ('US/Pacific', 'Pacific'),
        ('UTC', 'UTC'),
        ('US/Arizona', 'Arizona'),
    ]

    slug = models.SlugField(unique=True)
    name = models.TextField()
    domain = models.TextField()
    timezone = models.TextField(choices=TIMEZONE_CHOICES)
class FeedSpecification(FeedmachineModel):
    feed = models.ForeignKey(
        'Feed', 
        on_delete=models.CASCADE,
        related_name='specifications'
    )
    tags = models.ManyToManyField(
        'Tag',
        blank=True,
        related_name='specifications',
        help_text='<div style="color: blue;">Selecting no tags will cause the specification to only filter by item_type.</div><div style="color: green;">Save specification with feed selected to scope tags to the associated station.</div>'  # nopep8
    )
    video_tags = models.ManyToManyField(
        'VideoTag',
        blank=True,
        related_name='specifications',
        help_text='<div style="color: blue;">Selecting no video_tags will cause the specification to only filter by item_type if video is selected.</div><div style="color: green;">Save specification with feed selected to scope tags to the associated station.</div>'  # nopep8
    )
    item_types = ChoiceArrayField(
        models.IntegerField(choices=Item.TypeChoice.choices, default=list)
    )
class Tag(CmsObject, FeedmachineModel):
    name = models.TextField()
    station = models.ForeignKey('Station', on_delete=models.CASCADE)

    objects = TagManager()

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


class VideoTag(FeedmachineModel):
    name = models.TextField()
    station = models.ForeignKey('Station', on_delete=models.CASCADE)

    class Meta:
        unique_together = ('station', 'name')

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

admin.py

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    search_fields = ('station__name','name')

class FeedSpecificationForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(FeedSpecificationForm, self).__init__(*args, **kwargs)

        self.fields['item_types'].widget = forms.CheckboxSelectMultiple(
            choices=Item.TypeChoice.feed_specification_choices()
        )
        try:
            self.fields['tags'].queryset = Tag.objects.filter(
                station=self.instance.feed.station
            )
            print("FeedSpecificationForm station", self.fields['tags'])
            self.fields['video_tags'].queryset = VideoTag.objects.filter(
                station=self.instance.feed.station
            )
        except:
            self.fields['tags'].queryset = Tag.objects.none()
            self.fields['video_tags'].queryset = VideoTag.objects.none()

class FeedFeedSpecificationInline(admin.StackedInline):
    model = FeedSpecification
    form = FeedSpecificationForm
    readonly_fields = ('id', )
    extra = 1

class FeedFeedShowcaseInline(admin.StackedInline):
    model = FeedShowcase
    readonly_fields = ('id', )
    extra = 1

@admin.register(Feed)
class FeedAdmin(admin.ModelAdmin):
    search_fields = ('slug','station__name')
    list_filter = ('station__name',)
    inlines = [FeedFeedSpecificationInline, FeedFeedShowcaseInline] 
    class Media:
        js = ("feed_station_tag.js",)

class FeedSpecificationAdmin(admin.ModelAdmin):
    search_fields = ('feed__slug','tags__name')
    list_filter = ('feed__station__name',)
    form = FeedSpecificationForm
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        queryset = queryset.select_related(
            'feed'
        ).prefetch_related(
            'tags',
            'video_tags',
            'feed__station'
        )
        return queryset

ajax_file.js

jQuery(function($){
    $(document).ready(function(){
        $("#id_station").change(function(){
            console.log("station id", $(this).val());
            $.ajax({
                url: window.location.origin +"/api/v1/tags/",
                type:"GET",
                data:{station_id: $(this).val(),page_size: 500000},
                success: function(result) {
                    var result1 = result["results"];
                    console.log("tags for the given station ID are",result1);
                    var total_forms = $("#id_specifications-TOTAL_FORMS").val();
                    for(i=0;i< total_forms; i++){
                        var tag_id = "id_specifications-"+i+"-tags"
                        cols = document.getElementById(tag_id);
                        cols.options.length = 0;
                        for(var k in result1){
                            cols.options.add(new Option(result1[k]["name"],result1[k]["id"]));
                        }

                    }
                
                },
                error: function(e){
                    console.error(JSON.stringify(e));
                },
            });
        });
    }); 
});

jQuery(function($){
    $(document).ready(function(){
        $("#id_station").change(function(){
            console.log("station id", $(this).val());
            $.ajax({
                url:window.location.origin+"/api/v1/videotags/",
                type:"GET",
                data:{station_id: $(this).val(),page_size: 500000},
                success: function(result) {
                    var total_forms = $("#id_specifications-TOTAL_FORMS").val();
                    var result1 = result["results"];
                    console.log("Video tags for the given station ID are",result1);
                    for(i=0;i<total_forms; i++){
                        var video_tag_id = "id_specifications-"+i+"-video_tags"
                        cols = document.getElementById(video_tag_id);
                        cols.options.length = 0;
                        for(var k in result1){
                            cols.options.add(new Option(result1[k]["name"],result1[k]["id"]));
                        }

                    }
                
                },
                error: function(e){
                    console.error(JSON.stringify(e));
                },
            });
        });
    }); 
});

When you are binding the post data to the form (see note below), it would be along the lines of replacing this:

With something more like this:

    def __init__(self, *args, **kwargs):
        super(FeedSpecificationForm, self).__init__(*args, **kwargs)

        self.fields['item_types'].widget = forms.CheckboxSelectMultiple(
            choices=Item.TypeChoice.feed_specification_choices()
        )
        self.fields['tags'].queryset = Tag.objects.all()
        self.fields['video_tags'].queryset = VideoTag.objects.all()

so that the field-level checks will pass any potentially-valid data, allowing you to perform a more detailed test for the complete form.

Note: I’m not sure right off-hand how you identify the difference in an admin view. My guess (at least the first thing I’d try) is that I would replace the form attribute on the ModelAdmin class with a get_form method that can check the request to determine if it’s a get or a post, and return the appropriate version of the form. (Probably by adding a parameter to the form constructor that can be tested in the __init__ method.)

Sorry, I don’t understand where/why we need to find check if the request is POST or GET. I have disabled the field validation inside FeedSpecification as per your advice so ALL the tags, video tags are loaded when page is loaded for any station name. When User changes the station name, and AJAX call is triggered and the appropriate tags, video tags are loaded automatically. When user selects any tag/video tag and clicks save and continue we need to perform custom clean method on the form so that user chooses tags, video tags corresponding the station name. When the page is loaded once again after clicking save, the selected values are persisting but we still have the issue of all tags and video tags being loaded. My thought was if can we access inline form fields from the main Feed form that is feedspecification form fields (tags, video tags) inside the Feed form init filter tags, video tags based in station id. Please share your thoughts. My knowledge in Django is more of a beginner level. Sorry for the trouble. Thank you.

class FeedForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(FeedForm, self).__init__(*args, **kwargs)
    try:
            self.fields['tags'].queryset = Tag.objects.filter(
                station=self.instance.station.id
            )
            print("FeedSpecificationForm station", self.fields['tags'])
            self.fields['video_tags'].queryset = VideoTag.objects.filter(
                station=self.instance.station.id
            )
   except:
            self.fields['tags'].queryset = Tag.objects.none()
            self.fields['video_tags'].queryset = VideoTag.objects.none()

Because of this:

When you’re doing the GET for the form to be displayed, you only want to load a small set of tags for the form.

When you’re doing the POST, you want to allow any data to be entered, and validate it later during the process.

Hmm I understand. But requirement is even during POST, we should only see a small set of tags, video tags corresponding to the changed station name. Is there any alternate ways to achieve this?

You don’t see anything during the POST process, unless the form is invalid and needs to be redisplayed - in which case you have the opportunity to reset the value of the fields.

You may want to become more familiar with what all is happening in each of the server and the browser when a form is being submitted. You may want to take some time to review the docs at Working with forms | Django documentation | Django.

Also see my reply at problem with redirect - #4 by KenWhitesell for a brief overview of what happens during the HTTP request / response cycle with form processing.

Thank you for the explanation. I read through the docs and I understood what you meant. I have modified the code as below:

class FeedAdmin(admin.ModelAdmin):
    search_fields = ('slug','station__name')
    list_filter = ('station__name',)
    inlines = [FeedFeedSpecificationInline, FeedFeedShowcaseInline] 
    class Media:
        js = ("feed_station_tag.js",)
    #Method to parse the request and render the correct form
    def get_form(self, request, obj=None, **kwargs):
        if request.method == "GET":
            try:
              self.fields['tags'].queryset = Tag.objects.filter(
              station=self.instance.feed.station)
              self.fields['video_tags'].queryset = VideoTag.objects.filter(
              station=self.instance.feed.station )
            except:
               self.fields['tags'].queryset = Tag.objects.none()
               self.fields['video_tags'].queryset = VideoTag.objects.none()

        elif request.method == "POST":
          self.fields['tags'].queryset = Tag.objects.all()
          self.fields['video_tags'].queryset = VideoTag.objects.all()     
        return super().get_form(request, obj, **kwargs)

The request is parsed successfully but I get the error “‘NoneType’ object is not subscriptable” for the line self.fields['tags'].queryset = Tag.objects.filter( station=self.instance.feed.station) The reason being, the field ‘Tags’ or ‘Video tags’ does not belong to Feed model but it belongs to FeedSpecificationAdmin model which is embedded into Feed form as an inline form. As per screenshot below:

Which comes back to the same question I was wondering - If a model form has one or more inline forms dependent on each other how can we access form fields between the models?

I also tried to use get_form inside FeedSpecification form. In this scenario- The fields are accessible but the request object is not involved. It’s like when Feed form is loaded, the request is attached to the main model FeedForm but not the inline forms models.

How do I proceed?

Your get_form function needs to create an instance of the form before you can modify it. Then, the modifications your making to that form are made to the instance of the form, not “self”, which is the ModelAdmin class.

Side note: what you’ve got will work, but it’s not quite what I had in mind - but it’s a difference that doesn’t materially make a difference.

But the code I wrote below is throwing errors:

@admin.register(Feed)
class FeedAdmin(admin.ModelAdmin):
    search_fields = ('slug','station__name')
    list_filter = ('station__name',)
    inlines = [FeedFeedSpecificationInline, FeedFeedShowcaseInline] 
    class Media:
        js = ("feed_station_tag.js",)
    #Method to parse the request and render the correct form
    form = FeedForm
    def get_form(self, request, obj=None, **kwargs):
    
      if request.method == "GET":
        try:
              self.fields['tags'].queryset = Tag.objects.filter(
              station=self.station)
              self.fields['video_tags'].queryset = VideoTag.objects.filter(
              station=self.station)
        except:
              self.fields['tags'].queryset = Tag.objects.none()
              self.fields['video_tags'].queryset = VideoTag.objects.none()

      elif request.method == "POST":
          self.fields['tags'].queryset = Tag.objects.all()
          self.fields['video_tags'].queryset = VideoTag.objects.all()     
      return super().get_form(request, obj, **kwargs)

Is this because I did not create an instance of the form in in FeedAdmin and invoke get_form ? can you please show me an example ?

If you have:

class MyClass:
    pass

How do you create an instance of MyClass?
(See 9. Classes — Python 3.11.3 documentation if needed)

Thank you for the tips. I created the class instance and invoked get_form as below

@admin.register(Feed)
class FeedAdmin(admin.ModelAdmin):
    search_fields = ('slug','station__name')
    list_filter = ('station__name',)
    inlines = [FeedFeedSpecificationInline, FeedFeedShowcaseInline] 
    class Media:
        js = ("feed_station_tag.js",)
    #Method to parse the request and render the correct form
    form = FeedForm
    def get_form(self, request, obj=None, **kwargs):
      feed_specification_form = FeedSpecificationForm()
      feed_form = FeedForm()
      if request.method == "GET":
        try:
              feed_specification_form.fields['tags'].queryset = Tag.objects.filter(
              station=feed_form.fields['station'])
              feed_specification_form.fields['video_tags'].queryset = VideoTag.objects.filter(
              station=feed_form.fields['station'])
        except:
              feed_specification_form.fields['tags'].queryset = Tag.objects.none()
              feed_specification_form.fields['video_tags'].queryset = VideoTag.objects.none()

      elif request.method == "POST":
          feed_specification_form.fields['tags'].queryset = Tag.objects.all()
          feed_specification_form.fields['video_tags'].queryset = VideoTag.objects.all()     
      return super().get_form(request, obj, **kwargs)

I get the below error in the line feed_specification_form.fields['tags'].queryset = Tag.objects.filter( station=feed_form.fields['station'])

I printed the feed_form object and I get the following:

{'slug': <django.forms.fields.SlugField object at 0x10e21f4f0>,
 'station': <django.forms.models.ModelChoiceField object at 0x10e21fc10>,
 'ad_policy': <django.forms.models.ModelChoiceField object at 0x10e21f820>, 
'inject_carousel': <django.forms.fields.BooleanField object at 0x10e171310>}

If I am able to parse ModelChoiceField object ( ‘station’: <django.forms.models.ModelChoiceField object at 0x10e21fc10>) and get the id and then filter Tag model based on this Id, this error will be solved. How do I parse this object?

You do all this work to create your forms, but then you’re not returning them.

You’re calling super to get the default forms and ignoring everything you’ve just built.

Your references to feed_specification_form should be self.feed_specification_form so that it becomes an attribute on the object and provide access to it outside this function.

You’re also already creating an instance of feed_form in your method - you don’t need either the form=FeedForm attribute in the class definition or the call to super - you can return feed_form.

You should also be creating your FeedForm using the provided obj as the instance for the form, along with binding it to the POST data.

Side note: In the future, if you’re getting an error like this, it’s more useful to copy/paste the complete traceback from your console where you’re running runserver than to try and post the image of the error. There’s too much information obscured here to be truly useful.

Now, regarding the error:

Rather than trying to extract the data from the feed_form, you should be getting those values from the obj being edited.

And I just realized that you’re trying to do this within an in-line in the admin, not a common view - and that’s why I was thinking along different lines for an implementation.

All this work being done for the feed_specification_form needs to be done in the class constructing that form for the inline, not the admin class itself. Where you have this code now is probably not going to work the way you want it to here.

yeah all the changes are being done in the admin panel and inside the Feed admin form , I have Feedspecification as an inline model form. I am confused now. If you could show me some example code or post it would be helpful…Thank you