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
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