One of the only problems that has slowed us down while developing our CMS has been the steep learning curve associated with changing some functionality in the built-in admin without writing it as a custom view. This problem reared its head again when I was building the score module which essentially extends our calendar module (i.e., some calendar events happen to be athletic competitions, and athletic competitions have additional information like scores, etc.) The way we have laid things out, creating a score happens on its own page (separate from the main calendar editing page) and involves selecting the calendar item to establish the foreign key relationship between the score and calendar item. The problem was that the list of calendar items was too long – at best I could filter it so that it would only display sporting events, but I really needed to filter one step further to the specific sport.
Since the Score class requires a sport to be selected, it seemed as though there had to be some way to whittle down the list so that it would only display the calendar items for a particular sport. Not knowing a ton about Python, I began fishing around for solutions. Fortunately, we had already gotten some good things happening with Dojo, so I decided to manipulate the contents of the calendar item select box with a little Ajax sleight of hand. The first step was to add an onchange() event handler to the calendar item select box, which was easy enough to do through javascript:
if ( obj.attachEvent ) {
obj['e'+type+fn] = fn;
obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
obj.attachEvent( 'on'+type, obj[type+fn] );
}
else
obj.addEventListener( type, fn, false );
}
function beginswitch(){
addEvent(document.getElementById("id_sport"),'change',sendreq);
}
To add beginswitch() to the page itself I simply added a block inside the body tag which I populated with onload="beginswitch();". The sendreq() function that beginswitch() assigns to the select box uses Dojo request an updated list of calendar items.
dojo.io.bind({
url: '/path/to/get/new/list/?topic='+document.getElementById("id_sport").value,
handler: callBack //return values to the callBack() function
});
}
This sends a call to the views.py file with a URL variable of the sport to match. The function filters the returned values to calendar items that are games that have already happened for the specified sport.
if request.GET['topic']:
topicstr = request.GET['topic']
exist_scores = Score.objects.values('calendar_item').filter(sport=topicstr)
score_list = [0]
for existing in exist_scores:
score_list.append(int(existing['calendar_item']))
newevents =
Calendar.objects.all()
.filter(topic__id=topicstr)
.filter(startdate__lte=datetime.datetime.today())
.exclude(id__in=score_list)
.order_by('startdate')
returnval = ""
for event in newevents:
returnval = returnval + str(event.id) + "|" + event.__str__() + "|"
return HttpResponse(returnval)
The calendar events that are sent back are delimited by "|" because I have not yet found an effective means for sending complex data objects back and forth through XMLHttpRequests. With a unique character as a delimiter, I split the results with javascript on the page itself. It is also worth noting that since the value attribute of the option tags is different than the content of them, I need to return two variables for every one calendar item.
Having gotten the filtered list of calendar items back, the next step is to remove all the existing records (several hundreds) from the calendar drop-down and repopulate it with the new, improved, smaller list of events that were returned from my view function.
if(isIE) parentObj.innerHTML += newObj.outerHTML;
else parentObj.appendChild(newObj);
}
function callBack(type, data, evt){
if (type == 'error'){
alert('Error when retrieving data from the server!');
}
data_arr = data.split("|");
relatedObj = document.getElementById("id_calendar_item");
//while there are still options left in the select box, continue removing them
while(relatedObj.length)
relatedObj.remove(0);
optionObj = document.createElement("OPTION");
optionObj.value = "";
optionObj.text = "———";
addToPage(relatedObj,optionObj);
for(i = 0; i < data_arr.length; i+=2){
if(data_arr[i].length){
optionObj = new Option(data_arr[i+1],data_arr[i],false,false);
optionStr = '<option value="' + data_arr[i+1] + '">' + data_arr[i] + "";
optionObj = document.createElement("OPTION");
optionObj.value = data_arr[i];
optionObj.text = data_arr[i+1];
addToPage(relatedObj,optionObj);
}
}
}
There are several important things to note in the code above. First is that MSIE has its own way of doing some things. Since IE doesn't properly handle the appendChild() function and most good browsers (*cough*Firefox*cough*) don't understand the MS-specific .outerHTML attribute, we need to do a check on which browser we're using so that we can actually add the code that we want to the page. Next, the for loop that goes through our entire array of data that we worked so hard to retrieve has to loop two items at a time. Since the data that we returned has both an id and a value to display, respectively, we need to loop in increments of two allowing us to refer to the current index and the successive one when building our option list.
Voila! We have a select box of calendar items that displays only the games associated with a particular sport.
There may be more elegant ways to accomplish this extra filtering, but this is what we've been able to get to work.





August 9th, 2006 at 3:53 pm
Nice tip on getting some JavaScript in the admin!
Out of curiousity, had you tried adding an extra Manager method to the class that could inherit the score or sport class? If so, what sort of limitations did you run into?
I'm thoroughly enjoying the posts, keep them coming!
August 22nd, 2006 at 2:00 am
very good stuf you given, realy i appreciate your article.
Thanks
Regards
Vijay
November 17th, 2006 at 3:42 am
Hello, very nice thanks!
DO you have an idea why I can't filter rows in the Admin change list page?
I wrote a custom manager in my model that aim to override the get_query_set() method, but the admin seems to ignore It.
Have you any idea?
Thanks, regards,
Picio
March 29th, 2007 at 10:19 am
if that intrest you, I'm using toko for WYSIWYG (it's a free one)… http://toko-contenteditor.pageil.net
April 15th, 2008 at 1:03 am
i had an error dojo.io is null or not an object
what seems to be the prob?
November 28th, 2008 at 7:08 am
hi,
I tried this but it doesn't work for me.
In the django template i wrote
{% for field in adminform.fields %}
document.getElementById("{{ field.field.auto_id }}").onchange = function beginswitch(){
addEvent(document.getElementById("name"),'change',sendreq);
}
{% endfor %}
and then the fucntions addEvent, sendreq, addToPage, callback just the way you have written them replacing only calendar and sport with my fields.
at the url i put an url with / … / … / + document.getElementById("name").value and in urls.py i made it to call the function refilter in views.py
then it automatically gets back to the CallBack function in the template?
it seems to me that in doesn't get into callback function.
I'M VERY NEW AT THIS.
PLEASE HELP ME. SEND ME AN EMAIL PLEASE
THANK YOU,
MIHAI