2014-09-28

Add a button to export the search results in Django

I have a simple search page with a form at the top for the user to enter the search criteria (e.g. last name, first name, age, etc), and the search results are shown in the bottom half of the page.
What I want to do is to add a button to the form to export the search results as a CSV file. At first, I tried to have the search and export buttons post to two different URLs, but of course that did not work because an HTML form can only post to one and one only one URL.
Then I tried to have both the search button and the export button post to the same URL, and have the Django view handling the difference based on which button the user has clicked to submit the form, e.g. if the search button is clicked, then routes to the search view, else if the export button is clicked, then routes to the export view. It worked but there is a problem.
Since the user can change the search criteria before clicking the export button, and since it is a POST, the export view will execute the search based on the newly entered search criteria and export the new search results, which will be different from what the user sees in the bottom half of the search page. This is counter-intuitive and unacceptable to me.
My third attempt is to have the search and export button post to two different URLs. When a user clicks the search button, the search view will execute the search and save the search criteria (e.g. last name, first name, age, etc) in a Django session variable. If the user then clicks the export button, the export view will grab the search criteria from the session variable, execute the search, and export the results. It works, but it is not very portable because whenever I change the search form, I will need to change the views.
My next approach is to save the Django QuerySet in a session variable instead of the search criteria. It didn't work because a QuerySet is not JSON serializable. I cannot get Django to save the QuerySet in a session variable.
My final approach is to save the QuerySet query (rather than the search criteria) in a session variable. Then it works perfectly. I don't have to modify the views when the form changes. The export view grabs the QuerySet query from the session and then execute it as a raw SQL query. The performance hit of saving the QuerySet query is negligible, although the session variable itself will be wasted if the user eventually does not click the export button. But that is really just a small price to pay.
=== search view sample code ===

last_name = form.cleaned_data['last_name'] or None
first_name = form.cleaned_data['first_name'] or None
age = form.cleaned_data['age'] or None

if last_name: kw['last_name__icontains'] = last_name
if first_name: kw['first_name__icontains'] = first_name
if age: kw['age__exact'] = age

object_list = Person.objects.filter(**kw) # for the search
request.session['search_sql'] = str(Person.objects.filter(**kw).query) # for the export

=== export view sample code ===

sql = request.session.get('search_sql')
object_list = Person.objects.raw(sql)

2014-09-21

Reset button that works across postback to same page

By definition, a HTML Reset button does not work after a form is postback to the same page, because the posted back values then become the "default" values of the form.
Although you can use Javascript to reset all the fields in the form, I usually resort to a more simple way. Just create a button with an onClick handler to visit the same page. The difference this time is that it will be a GET instead of a POST, so all fields in the form will be cleared and reset to the original defaults.
Yes, it takes a round trip to the server, but the logic is simple and portable.
=== Sample Code ===

<input type="button" onclick="window.location=''" value="Reset" />

2014-09-15

Using a datepicker in Django model form

If your front end framework (e.g. Bootstrap) has a datepicker CSS class that you would like to use in a Django model form, how can you do that?
It turns out we can add any attribute to a HTML input element in a Django model form via the formfield_callback defined in the model form. We can make use of this technique to add the datepicker class (an attribute) to a model DateField (an HTML input element).
The following is an example of adding the datepicker class to all the DateFields in a model form. You will also have to make sure the datepicker class definition is available, probably by including the front end framework files in the base template.
=== forms.py (model datefield) ===

from django.forms import ModelForm
from django.db import models
from .models import MyModel

def make_custom_datefield(f):
    # f is a model field
    # if f is a date field, add the datepicker class to it
    # return a form field
    formfield = f.formfield()
    if isinstance(f, models.DateField):
        formfield.widget.attrs.update({'class':'datePicker'})
    return formfield

class MyForm(ModelForm):
    formfield_callback = make_custom_datefield
    class Meta:
        model = MyModel

=== html rendered ===

<input class="datePicker" id="id_datefield" name="datefield" type="text" value="" />
How about adding the datepicker class to a form DateField (instead of a model DateField)? We can just add an attribute directly in the form DateField definition. The following is an example of it.
=== forms.py (form datefield) ===

class MyForm(ModelForm):
    somedate = forms.DateField(
                 widget=forms.TextInput(attrs={'class':'datePicker'}))
    class Meta:
        model = MyModel

EDITED 2015-07-07 If the form uses a custom widget, add a keyword argument to the formfield_callback function like below, otherwise you will have a "make_custom_datefield() got an unexpected keyword argument 'widget'" error.
def make_custom_datefield(f, **kwargs):
    formfield = f.formfield(**kwargs)
    if isinstance(f, models.DateField):
        formfield.widget.attrs.update({'class':'datePicker'})
    return formfield

2014-09-07

Leverage Django Permission System

Django comes with a simple permission system which is used by the Django admin site. The doc says we can use the Django permission system in our own code as well.
How can we leverage the Django Permission System for our own use? One way is to create an app specifically for the purpose, and use a model to create the custom permissions that we need.
For example, if we want a user to have "export" permission in order to export data, and "download" permission in order to download stuff, then we can create an app called "permission" with a model called "Permission", and add custom permissions to it.
After adding the "permission" app to INSTALLED_APPS in settings.py and run syncdb, we can then use Django admin site to assign permission to a user, or better yet assign permissions to a group and then assign users to the group. Then we can check if a user has "export" or "download" permission by using the Django permission system.
Creating an app for this purpose may be an overkill, but I found it modular in design and easy to maintain.

=== permission/models.py ===

from django.db import models

class Permission(models.Model):
    class Meta:
        permissions = (
            ("export", "Can export data"),
            ("download", "Can download stuff"),
        )

=== settings.py ===

INSTALLED_APPS = (
    ...
    'permission',
)

=== Usage ===

from django.contrib.auth.models import User
john = User.objects.get(username='john')
john.has_perm("permission.export")

or

@permission_required('permission.export')
def some_view(request):
    ...

-End-

2014-09-01

Show the Django project version in the login page

I would like to display my Django project version string in the login page. However my project uses the Django Authentication System, which provides a login view (django.contrib.auth.views.login) which in turn uses my template file. Since I do not have control over the login view, how can I pass the version string into the template?
Turn out that the login view is a generic view and all generic views take an extra optional parameter, extra_context, which is a dictionary of objects that will be added to the template's context.
All we have to do is to add the version string to extra_context. We can do that in the urls.py. The url() function takes an optional argument which is a dictionary of extra keyword arguments to pass to the view function.
=== urls.py ===

VERSION = "1.0"

urlpatterns = patterns('',
    ...
    url(r'^accounts/login/$', 'django.contrib.auth.views.login', {'extra_context': {'version':VERSION}}),
)

=== template.html ===

{% extends "base.html" %}
{% block title %}Project Name {{ version }}{% endblock %}
{% block content %}{% endblock %}