I recently reworked the Djukebox UI. It's still ugly, but it uses way cooler things behind the scenes. The original incarnation had a main page which housed the player, a hidden iframe for uploading new tracks while letting the user continue browsing and listening, and another iframe for the current page content so that the player could keep playing while the user clicks around through lists of tracks and albums.

It worked pretty smoothly, but it's not cool and new. This is an old, hacky way of doing things. The point of Djukebox is to be a playground to do new things. The new solution is more interesting and adds even more options that can be implemented in the future. I decided to add a REST API and use a combination of ajax calls to that new API and a JavaScript templating system to update the page. The final combination I ended up with was Tastypie for the REST API and Handlebars.js for the JavaScript templates.

Tastypie was picked because its up to date and being actively developed as well as mostly straightforward and easy to use. Tastypie was easy to extend and add the functionality I needed to when it was missing it. The most interesting bit was making the details of related resources toggleable. By default the related resource details are either never shown or always shown as defined on the field in your resource. I wanted to be able to specify in the URL whether to give me details or not. What I ended up with was a little mixin based on this blog which allows me to add a "details" querystring parameter which takes a comma separated list of related resources to show the details of.

inlinetogglemixin.py:

class InlineToggleMixIn(object):
    """
    Allows details=<resource> querystring param to toggle whether to return resource uri or full details
    """

    # based on handy dehydrate_related from
    # http://chrismeyers.org/2012/06/25/tastypie-inline-aka-nested-fulltrue-embedded-relationship-dynamically-via-get-parameter/
    # Be careful not to create an infinite loop from having multiple resources that reference each other using this
    # and requesting details on both of them
    def dehydrate_related(self, bundle, related_resource):

        # this is not 100% ideal because the resource_name and collection_name may not be
        # what the end user sees, depending on how things are named. They see the attribute name
        resource_name = related_resource._meta.resource_name
        collection_name = related_resource._meta.collection_name
        inlines = bundle.request.GET.get('details','').split(',')

        if resource_name in inlines or collection_name in inlines:
            bundle = related_resource.build_bundle(obj=related_resource.instance, request=bundle.request)
            return related_resource.full_dehydrate(bundle)

        # default behavior for this method
        return related_resource.get_resource_uri(bundle)

# Use these where you would have used a ToManyField or ToOneField
class ToManyFieldDetailToggle(InlineToggleMixIn, fields.ToManyField):
    pass

class ToOneFieldDetailToggle(InlineToggleMixIn, fields.ToOneField):
pass

This isn't perfect. As the comments say, it uses the resource_name and collection_name, but those are not necessarily how they are named in the response. You also run the risk of creating an infinite loop of related resources, although fortunately Tastypie will catch that and blow up long before it eats up your server resources.

The handlebars templates also presented some interesting issues. The standard way of using handlebars templates is that you put the html template inside <script> tags in your html. Unfortunately, handlebars templates use {{tag}}, just like Django. When this is also a Django template, then the Django parser tries to deal with these tags. The solution to this is to put the templates into separate files, which is fine by me since I prefer separating them out anyway, and then use ajax to pull those in. I wrote some JavaScript which downloads my templates using ajax requests and then compiles them and sticks the compiled template into another JavaScript object so that I don't have to continually download and recompile them. Just to be safe, prior to using a template, I double check to make sure the cache object has the template there and if not, I download it with my function to update the display as a callback.

Here's a slightly shortened example.

handlebars_ajax_templates.js:

$(document).ready(function() {
    // lots of other stuff in the full code
    var compiled_templates = new Object();
    var ALBUMLIST = 'album-list.js';  // real code has several more of these.  It's the filename of a handlebars.js template

    function getTemplateAjax(file) {
    // downloads a handlebars.js template via ajax and sticks it into the cache object after compiling
        var template; // this could really go down in the success function

        return $.ajax({
                url: "{{ STATIC_URL }}djukebox/js/templates/" + file, //ex. js/templates/mytemplate.handlebars
                cache: true,
                dataType: "text",
                success: function(data) {
                    template = Handlebars.compile(data);
                    compiled_templates[file] = template;
                },
         });
    }
   
    function switchView(template, context, callback){
    // takes the name of the template file, the context to pass into it, and an optional callback
    // checks to make sure the compiled template is cached, otherwise uses jquery deferred to
    // to download the template and compile it and then the function calls itself again.
        if(template in compiled_templates) {
            $('#content').html(compiled_templates[template](context));
            if(typeof callback === 'function') {
            // We may have things to do after the template has been injected into the page
            // such as bind events to newly added elements.  We only want to do this after injection
            // and not when we had to reload the template as below, so we can't just call this function
            // from inside .when() and use .then() to do the bindings.
                callback();
            }
        } else {
            $.when(getTemplateAjax(template, context)).then(function() {
                switchView(template, context, callback);
            });
        }
    }

    // load in a new view by doing something like so
    // someAjaxCall() may make a call to an API to get the data used
    // for the template context, like Djukebox does, or just about anything
    // which needs to happen first.
    $.when(someAjaxCall()).then(function(data){
        // now that we have data for the template context, swap in the new view
        switchView(ALBUMLIST, data);
    });
});