Tweaking Django Admin with Javascript

     

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:

function addEvent( obj, type, fn ) {
                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.

function sendreq(){
  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.

def refilter(request):
        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.

function addToPage(parentObj,newObj){
        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.

6 Responses to “Tweaking Django Admin with Javascript”

  1. Adam Spooner Says:

    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!

  2. Vijay Says:

    very good stuf you given, realy i appreciate your article.

    Thanks
    Regards
    Vijay

  3. Picio Says:

    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

  4. free content management system Says:

    if that intrest you, I'm using toko for WYSIWYG (it's a free one)… http://toko-contenteditor.pageil.net

  5. rob Says:

    i had an error dojo.io is null or not an object
    what seems to be the prob?

  6. Mihai Says:

    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

Leave a Reply