Djblets and Review Board moving to jQuery

When we first began development of Review Board and its Django utility package Djblets almost two years ago, we needed to pick a JavaScript library to use. There were several good ones available at the time, and after evaluating several options we chose the Yahoo! UI Library (YUI) and YUI-Ext, an extension library to YUI. Both were excellent and have served us well over the past two years.

However, YUI-Ext itself is really no more, which means we needed to choose something new. We could have continued to bundle it and YUI with Review Board, but users of Djblets would have to hunt down a copy and load in all of YUI and YUI-Ext just to use such features as datagrids. This was, to say the least, inconvenient.

YUI-Ext and the Licensing Situation

Now I said YUI-Ext is no more, but really it’s still around, just in a new form. It had been renamed to ExtJS and became its own independent library. While compatible with YUI, most of the functionality we needed was really provided by ExtJS, meaning we didn’t really need both. Over time, YUI evolved as well, so we began looking into the differences and figuring out which to go with.

ExtJS, it turns out, was a non-starter for us. Unlike YUI-Ext before it, the ExtJS licensing terms were a bit restrictive and, frankly, confusing. Open source projects could use it under the GPL3, but commercial products required a special license per developer, which is $289 per developer. The GPL3 itself is unclear with regards to our situation. We’re an open source project, but we’re MIT-licensed. What if a company wants to modify something for their own use and not release it to the public? What if down the road someone wants to offer commercial extensions to Review Board? We’d have to choose the commercial license to be safe, but then contributors would also need to pay $289 for a license, wouldn’t they?

Looking Again at YUI

We didn’t want to step down a dangerous road with ExtJS, so we started to look again at YUI. It’s definitely come a long way and I must recommend it for developers looking for a strong toolkit with good UI support. They provide good documentation, many examples, and base functionality for nearly everything.

However, there’s a lot we’d have to rewrite to move to it, and if we had to rewrite the code anyway, I wanted to look around a bit at the current generation of JavaScript toolkits. Especially since our needs have changed over the years and we’re looking for something with a lighter footprint. We’re also moving away from the dialog-centered UI we have on some pages, which is where YUI shines.

jQuery

We ended up deciding to go with jQuery, partially because of the footprint and partially the clean way in which scripts can be written. As an experiment, I converted our Djblets datagrid code to jQuery. The result is a smaller, much more readable, reusable, cross-browser script. I was impressed by what jQuery let me do, and by the size.

I did some comparisons on the number of files downloaded and the size of the files, using the Review Board dashboard as a test.

With YUI and YUI-Ext, we were loading 7 JavaScript files, excluding our own scripts, at a total size of 376KB (when minified).

With jQuery, we loaded only 2 JavaScript files, at a total size of 128KB.

Our datagrid.js file also went down from 13KB to 9KB largely due to some of the niceties of jQuery’s API.

This is a pretty significant savings, a whole 252KB, which could be a couple of seconds on an average DSL line. And it’s not just the file size but the number of requests, which can make a big difference on a heavily accessed web server.

What This Means for Djblets

Developers using Djblets can now use datagrids without needing to do anything special. Djblets bundles jQuery, which it will use by default unless you choose to override which jQuery script is being used. This means all you need is an install of Djblets set up and you’re ready to use it!

Going Forward

We’re working on migrating the rest of Review Board to jQuery. This will take place over the next few weeks, and is one of the last major things we hope to do for our 1.0 release.

Django Development with Djblets: Data Grids

It’s been a while since my last post in this series, but this one’s a good one.

A common task in many web applications is to display a grid of data, such as rows from a database. Think the Inbox from GMail, the model lists from the Django administration interface, or the Dashboard from Review Board. It’s not too hard to write something that displays a grid by doing something like:


{% for user in users %}
 
{% endfor %}
Username First Name Last Name
{{user.username}} {{user.first_name}} {{user.last_name}}

This works fine, so long as you don’t want anything fancy, like sortable columns, reorderable columns, or the ability to let users specify which columns they want to see. This requires something a bit more complex.

djblets.datagrids

We wrote a nifty little set of classes for making data grids easy. Let’s take the above example and convert it over.

# myapp/datagrids.py
from django.contrib.auth.models import User
from djblets.datagrid.grids import Column, DataGrid

class UserDataGrid(DataGrid):
    username = Column("Username", sortable=True)
    first_name = Column("First Name", sortable=True)
    last_name = Column("Last Name", sortable=True)

    def __init__(self, request):
        DataGrid.__init__(self, request, User.objects.filter(is_active=True), "Users")
        self.default_sort = ['username']
        self.default_columns = ['username', 'first_name', 'last_name']
# myapp/views.py
from myapp.datagrids import UserDataGrid

def user_list(request, template_name='myapp/datagrid.html'):
    return UserDataGrid(request).render_to_response(template_name)

Now while this may look a bit more verbose, it offers many benefits in return. First off, users will be able to reorder the columns how they like and choose which columns to see. You could extend the above DataGrid to add some new column but leave it out of default_columns so that only users who really care about it would see it.

Custom columns

Let’s take this a step further. Say we want our users datagrid to optionally show staff members with a special badge. This might be useful to some users, but not all, so we’ll leave it out by default. We can implement this by creating a custom column with image data:

class StaffBadgeColumn(Column):
    def __init__(self, *args, **kwargs):
        Column.__init__(self, *args, **kwargs)

        # These define what will appear in the column list menu.
        self.image_url = settings.MEDIA_URL + "images/staff_badge.png"
        self.image_width = 16
        self.image_height = 16
        self.image_alt = "Staff"

        # Give the entry in the columns list menu a name.
        self.detailed_label = "Staff"

        # Take up as little space as possible.
        self.shrink = True

    def render_data(self, user):
        if user.is_staff:
            return '%s' % 
                (self.image_url, self.image_width, self.image_height, self.image_alt)

        return ""

class UserDataGrid(DataGrid):
    ...
    staff_badge = StaffBadgeColumn()
    ...

This will add a new entry to the datagrid’s column customization menu showing the staff badge with a label saying “Staff.” If users enable this column, their datagrid will update to show an staff icon for any users who are marked as staff.

Custom columns can render data in a couple of different ways.

The first is to bind the column to a field in the model. By default, a Column instance added to a DataGrid will use its own name (such as “first_name” above) as a lookup in the object. If you want to use a custom field, set the db_field attribute on the column. This field can span relationships as well. For example:

class UserDataGrid(DataGrid):
    ...
    # Uses the field name as the lookup.
    username = Column("Username")

    # Spans relationships, looking up the age in the profile.
    age = Column("profile__age")

The second is the way shown above, by overriding the render_data function. This takes an object, which is the object the datagrid represents, and outputs HTML. The logic in this can be quite complicated, depending on what’s needed.

Linking to objects

A datagrid is pretty worthless if you can’t link entries for the object to a URL. We have a couple of ways to do this.

To link a column on a datagrid to a URL, set the link parameter on the DataGrid to True. By default, the URL used will be the result of calling get_absolute_url() on the object represented by the datagrid, but you can override this:

# myapp/datagrids.py

class UserDataGrid(DataGrid):
    username = Column("Username", sortable=True, link=True)
    ...
    @staticmethod
    def link_to_object(user, value):
        return "/users/%s/" % user

Then

link_to_object takes an object and a value. The object is the object being represented by the datagrid, and the value is the rendered data in the cell. The result is a valid path or URL. In the above example, we’re explicitly defining a path, but we could use Django’s reverse function.

Usually the value is ignored, but it can be useful. Imagine that your Column represents a field that is a ForeignKey of an object. The contents of the cell is the string version of that object (implemented by the __str__ or __unicode__ function), but it’s just an object and you want to link to it. This is where value comes in handy, as instead of being HTML output, it’s actually an object you can link to.

If you intend to link a particular column to value.get_absolute_url(), we provide a handy utility function called link_to_value which exists as a static method on the DataGrid. You can pass it (or any function) to the Column’s constructor using the link_func parameter.

class UserDataGrid(DataGrid):
    ...
    myobj = Column(link=True, link_func=link_to_value)

Custom columns can hard-code their own link function by explicitly setting these values in the constructor.

Saving column settings

If a user customizes his column settings by adding/removing columns or rearranging them, he probably expects to see his customizations the next time he visits the page. DataGrids can automatically save these settings in the user’s profile, if the application supports it. Handling this is as simple as adding some fields to the app’s Profile model and setting a couple options in the DataGrid. For example:

# myapp/models.py

class Profile(models.Model):
    user = models.ForeignKey(User, unique=True)

    # Sort settings. One per datagrid type.
    sort_user_columns = models.CharField(max_length=256, blank=True)

    # Column settings. One per datagrid type.
    user_columns = models.CharField(max_length=256, blank=True)
# myapp/datagrids.py

class UserDataGrid(DataGrid):
    ...
    def __init__(self, request):
        self.profile_sort_field = "sort_user_columns"
        self.profile_columns_field = "user_columns"

The DataGrid code will handle all the loading and saving of customizations in the Profile. Easy, isn’t it?

Requirements and compatibility

Right now, datagrids require both the Yahoo! UI Library and either yui-ext (which is hard to find now) or its replacement, ExtJS.

Review Board, which Djblets was mainly developed for, still relies on yui-ext instead of ExtJS, so datagrids today by default are built with that in mind. However, the JavaScript portion of datagrids should work with ExtJS today. The default template for rendering the datagrids (located in djblets/datagrids/templates/datagrids/) assumes both YUI and yui-ext are present in the media path, but sites using datagrids are encouraged to provide their own template to reflect the actual locations and libraries used.

We would like to work with more JavaScript libraries, so if anyone wants to provide an implementation for their favorite library or help to dumb down our implementation to work with any and all, we’d welcome patches.

See it in action

We make extensive use of datagrids on Review Board. You can get a sense of it and play around by looking at the users list and the dashboard (which requires an account).

That’s it for now!

There’s more to datagrids than what I’ve covered here, but this should give you a good understanding of how they work. Later on I’d like to do a more in-depth article on how datagrids work and some more advanced use cases for them.

For now, take a look at the source code and Review Board’s datagrids for some real-world examples.