2012-10-09

How to Set Hijax with Django and jQuery

The main principle of progressive enhancement is that the markup, styling, and Javascript are separated. The site should be functional and browsable without Javascript, but when Javascript is activated, it can add additional functionality to the elements of a page. For example, it can hijack the default behavior of normal links replacing it with Ajax loading of just a specific part of the page. This type of progressive enhancement was called Hijax by Jeremy Keit.

Hijax has the following benefits:

  • Visitors can browse the content on any browser and on any platform.
  • The content is indexable by search engines for each page.
  • Open graph lets you share each page on Facebook with specific title, description and image.

Let me show you an example of Hijax. Recently during my spare time I developed a simple collective storytelling game 1000 Words. Every participant at any point of a story has a chance to choose the continuation previously entered by someone else or to continue by his own words. It's like threaded comments where you see only one thread at a time.

All continuations are browsable without Javascript and each of them has Open Graph set, so Google can index all variations of the stories and any page can be shared on Facebook with custom title, description, and image. When Javascript is enabled (which is the usual case for human visitors), clicking on one of the given continuation choices doesn't refresh the full page, but rather loads the piece of the story and further possible continuations by Ajax and injects into a proper place. This way, I don't need to load all the history with each request and the visitor has more pleasant experience.

Django HttpRequest has a method to check if the call to a page was made by Ajax or by the normal browsing. It's the request.is_ajax() method. Depending on the call, the view of a page can provide different templates: one containing the full HTML with head and body, and another one just with the required snippet of HTML.

There are two ways to organize your templates for Hijax in Django without repeating yourself. One way is to include the Ajax snippet into the template of non-Ajax version.

# views.py
def my_view(request, ...):
    # ...
    template = "myapp/story.html"
    if request.is_ajax():
        template = "myapp/story_ajax.html"
    # ...   
    return render(request, template, context_dict)
{# story.html #}
{% extends "base.html" %}

{% block content %}
    {% include "myapp/story_ajax.html" %}
{% endblock %}
{# story_ajax.html #}
<div class="dynamic-content">
    ...
</div>

The other way to organize your templates for Hijax is extend different bases for normal and Ajax cases. The normal base would have all usual HTML and body, whereas the base Ajax template would just have empty block.

# views.py
def my_view(request, ...):
    # ...
    if request.is_ajax():
        context_dict['base_template'] = "base_ajax.html"
    # ...   
    return render(request, "myapp/story.html", context_dict)
{# story.html #}
{% extends base_template|default:"base.html" %}

{% block content %}
    <div class="dynamic-content">
        ...
    </div>
{% endblock %}
{# base_ajax.html #}
{% block content %}
{% endblock %}

Last but not least, it's time to add unobtrusive Javascript which replaces the default behavior of the links with Ajax calls. With jQuery it should be something like this:

// hijax.html
(function($, undefined) {
    $(document).ready(function() {
        // hijack all links with the css class "dynamic"
        $('a.dynamic').live('click', function() {
            $('#content').load($(this).attr('href'));
            return false;
        });
    });
})(jQuery);

This is not the exact code from the game, but it illustrates the concept of progressive enhancement very well.

2012-10-05

Quick Tip on Setuptools

You are probably used to the default usage of Setuptools, for example the following command installs the latest Django version into your python installation or currently active virtual environment:

easy_install django

Sometimes you might need to install a specific version of a module. This is an example of installing Django 1.3.3:

easy_install django==1.3.3
If you want to upgrade the existing installation to the latest version, you can do that with:
easy_install -U django

2012-09-27

Initial Data Using South Migrations

Usually when starting a new project with commonly used apps, you will need some default data for the database, for example, default categories. Django provides fixtures for that. A fixture is a collection of data that Django knows how to import into a database.

They say:

If you create a fixture named initial_data.[xml/yaml/json], that fixture will be loaded every time you run syncdb.

There is one problem with this approach. It is very likely that you will need to run syncdb in the future when adding new apps (without south migrations). In such cases the modified data will be overwritten with the initial data from fixtures.

But there is a solution for that problem. You need to name your fixtures differently and then import them by south migrations. If you don't use south, you should start doing that, because it is a very handy way to modify database schema and data automatically. :)

So this is how you can prepare south migration with the default fixture:

1. Create a fixture from existing models.

python manage.py dumpdata --format=json --indent=4 myapp.Category > /path/to/myapp/fixtures/myapp_categories.json

2. Create data migration

python manage.py datamigration myapp load_default_data

3. Edit data migration

class Migration(DataMigration):
    
    def forwards(self, orm):
        from django.core.management import call_command
        call_command("loaddata", "myapp_categories.json")
    
    def backwards(self, orm):
        pass

4. Migrate

python manage.py migrate myapp

2011-12-16

The Usage of the only() Method

Recently I found the only() method of a queryset which can significantly improve the loading speed of forms with ModelChoiceField or ModelMultipleChoiceField. Let's say we have a big Event model with thousands of instances:

class Event(models.Model):
    title = models.CharField(_("Title"), max_length=200)
    # ... lots of other fields ...
    def __unicode__(self):
        return self.title
Then we have a form with ModelMultipleChoiceField:

class EventForm(forms.Form):
    related_events = forms.ModelMultipleChoiceField(
        queryset=Event.objects.all().only("id", "title"),
        label=_("Related Events"),
        required=False,
        )
    # ... other fields of the form ...
The used only() method loads only the primary key and the title for the selection widget.

2011-11-21

Custom Admin Filter Specification

Let's say we have a model Project with age groups as boolean fields:

class Project(models.Model):
    # ...
    age_group_0_5 = models.BooleanField(u"From 0 to 5 years")
    age_group_6_12 = models.BooleanField(u"From 6 to 12 years")
    age_group_13_18 = models.BooleanField(u"From 13 to 18 years")
    age_group_19_27 = models.BooleanField(u"Fron 19 to 27 years")
    # ...

In order to list them as one selection in the admin list filter, we need a specific filter specification. Let's create it in the admin.py:

from django.contrib.admin.filterspecs import FilterSpec

class AgeChoicesFilterSpec(FilterSpec):
    def title(self):
        return u"Age group"
        
    def __init__(self, f, request, params, model, model_admin,
                 field_path=None):
        super(AgeChoicesFilterSpec, self).__init__(f, request, params, model,
                                                model_admin,
                                                field_path=field_path)
        self.request = request
        
    def choices(self, cl):
        yield {'selected': not(
                    self.request.GET.get('age_group_0_5', None) or 
                    self.request.GET.get('age_group_6_12', None) or 
                    self.request.GET.get('age_group_13_18', None) or 
                    self.request.GET.get('age_group_19_27', None) 
                    ),
               'query_string': cl.get_query_string(
                               {},
                               ['age_group_0_5', 'age_group_6_12', 'age_group_13_18', 'age_group_19_27']),
               'display': u'All'}
        yield {'selected': self.request.GET.get('age_group_0_5', None) == 'True',
               'query_string': cl.get_query_string(
                               {'age_group_0_5': 'True'},
                               ['age_group_6_12', 'age_group_13_18', 'age_group_19_27']),
               'display': u"From 0 to 5 years"}
        yield {'selected': self.request.GET.get('age_group_6_12', None) == 'True',
               'query_string': cl.get_query_string(
                               {'age_group_6_12': 'True'},
                               ['age_group_0_5', 'age_group_13_18', 'age_group_19_27']),
               'display': u"From 6 to 12 years"}
        yield {'selected': self.request.GET.get('age_group_13_18', None) == 'True',
               'query_string': cl.get_query_string(
                               {'age_group_13_18': 'True'},
                               ['age_group_0_5', 'age_group_6_12', 'age_group_19_27']),
               'display': u"From 13 to 18 years"}
        yield {'selected': self.request.GET.get('age_group_19_27', None) == 'True',
               'query_string': cl.get_query_string(
                               {'age_group_19_27': 'True'},
                               ['age_group_0_5', 'age_group_6_12', 'age_group_13_18']),
               'display': u"From 19 to 27 years"}

FilterSpec.filter_specs.insert(0, (lambda f: f.name == 'age_group_0_5',
                               AgeChoicesFilterSpec))

Here the most important method of FilterSpec is choices. It defines the choices for the filter by dictionary with three keys:

  • selected is the condition when this choice should be selected.
  • query_string is the URL query string that will be formed to filter the list by. The query string is formed by get_query_string method of ChangeList instance. It takes two parameters: a dictionary of new parameters to add to the current query, and a list of parameters to remove from the current query.
  • display is the human-readable string which will be shown for this choice in the filter.

The new filter specification should be attached to a field. Here it will be applied to all fields with the name "age_group_0_5" and we have just one such field.

The last thing to do is to define admin options for the Project model:

from django.contrib import admin
from models import Project

class ProjectAdmin(admin.ModelAdmin):
    # ...
    list_filter = ("age_group_0_5", )
    # ...
    
admin.site.register(Project, ProjectAdmin)
Now you know how to create a custom admin filter specification for your model. :)

2011-11-11

Django CMS, Haystack, and Custom Plugins

When you need a full-text search for Django-CMS-based website, you can use Haystack and django-cms-search. The latter module ensures that all CMS Pages get indexed.

One important thing to mention is that if you use any custom Plugins, search_fields need to be defined for them, so that the Pages using them are indexed properly. For example, here is an Overview plugin which makes its title and description searchable:


from django.db import models
from django.utils.translation import ugettext_lazy as _
from cms.models import CMSPlugin

class Overview(CMSPlugin):
    title = models.CharField(_('Title'), max_length=200)
    description = models.TextField(_('Description'))
    url = models.URLField(_('url'), max_length=200, blank=True)
    
    search_fields = ("title", "description")
    
    def __unicode__(self):
        return self.title
        

For more information check the documentation online:

2009-10-20

Weather App on Github

As promised, I put the code of the climate_change app online. For that reason, I created an account on github trying to use Git for the first time. This is also the first time I am using a distributed version control system, i.e. you can create branches of the project, develop the forks separately on different computers, and merge them in any order. There is no main centralized repository. The code on github that I uploaded is just one initial copy.

I am still new to Git commands and workflows. You can share your knowledge with me. For example, maybe you know some good cheat sheet or writings about Git. Or you can try to fork the project I committed, change it somehow and then push for merging (not sure if I'm using correct jargon). I will share with you what I find interesting and useful about Git while learning it.

2009-10-19

Weather App Tutorial. Part 5 of 5. Graph View

I had some problems with the internet, so I am posting this entry a little later that expected. But anyway, this is it - the final part of the tutorial about the weather app. You can read the first four parts here: 1, 2, 3, 4. Today I will show you how to display a chart with statistical data from the collected weather details.

I googled a little to find out what charting system to use for this small project. And I found Open Flash Chart as an interesting solution. It's a flash-based charting system which uses Ajax requests to get the data for the graphs. That means that we'll need a view which will show the graphs and another view, which will provide the data. Each location will have its own page displaying a chart, so it would be nice to have URLs where each location would be accessed by a slug. We already have a field sysname which could be used here, but my best practice is to have separate fields for url slugs which might be changed at anytime and sysnames which should be defined once and not changed at all because the sysname binds an object with a template.

So I add this new field to the Location model in models.py:

class Location(models.Model):
#...
slug = models.SlugField(
_("slug for urls"),
max_length=200,
unique=True,
blank=True,
default="",
)
#...


Then I create and apply a migration for it:

python manage.py startmigration add_slug --auto
python manage.py migrate climate_change


Let's run the webserver once again. Go to http://127.0.0.1:8000/admin/climate_change/location/1/, add "berlin" as a slug and save the record.

Next I need to define a view which will display the graphs. This looks as simple as this:

# -*- coding: UTF-8 -*-
from django.shortcuts import get_object_or_404, render_to_response

Location = models.get_model("climate_change", "Location")

def display_charts(request, slug):
location = get_object_or_404(Location, slug=slug)
return render_to_response(
"climate_change/charts.html",
{'location': location},
context_instance=RequestContext(request),
)


Whereas the template created at templates/climate_change/charts.html includes this code:


{% extends "base.html" %}

{% load i18n %}

{% block title %}{{ location.name }}{% endblock %}

{% block header %}
</code><h1>{{ location.name }}</h1>
{% endblock %}

{% block content %}
<div class="chart-temperature">
<h2>{% trans "Temperature" %}</h2>
<object type="application/x-shockwave-flash" data="/media/flash/open-flash-chart.swf?data-file={% url climate_change_temperature slug=location.slug %}" height="250" width="500">
<param name="movie" value="/media/flash/open-flash-chart.swf?data-file={% url climate_change_temperature slug=location.slug %}">
<param name="allowScriptAccess" value="sameDomain">
<param name="quality" value="high">
</object>
</div>
<div class="chart-humidity">
<h2>{% trans "Humidity" %}</h2>
<object type="application/x-shockwave-flash" data="/media/flash/open-flash-chart.swf?data-file={% url climate_change_humidity slug=location.slug %}" height="250" width="500">
<param name="movie" value="/media/flash/open-flash-chart.swf?data-file={% url climate_change_humidity slug=location.slug %}">
<param name="allowScriptAccess" value="sameDomain">
<param name="quality" value="high">
</object>
</div>
<div class="chart-wind-speed">
<h2>{% trans "Wind speed" %}</h2>
<object type="application/x-shockwave-flash" data="/media/flash/open-flash-chart.swf?data-file={% url climate_change_wind_speed slug=location.slug %}" height="250" width="500">
<param name="movie" value="/media/flash/open-flash-chart.swf?data-file={% url climate_change_wind_speed slug=location.slug %}">
<param name="allowScriptAccess" value="sameDomain">
<param name="quality" value="high">
</object>
</div>
<div class="chart-visibility">
<h2>{% trans "Visibility" %}</h2>
<object type="application/x-shockwave-flash" data="/media/flash/open-flash-chart.swf?data-file={% url climate_change_visibility slug=location.slug %}" height="250" width="500">
<param name="movie" value="/media/flash/open-flash-chart.swf?data-file={% url climate_change_visibility slug=location.slug %}">
<param name="allowScriptAccess" value="sameDomain">
<param name="quality" value="high">
</object>
</div>
{% endblock %}


Also let's not forget to download the Open Flash Chart flash file itself which is in the archive of this package. I placed the file open-flash-chart.swf at media/flash.

Originally, Open Flash Chart uses PHP on the server side to provide data to the flash-based client side. But fortunately there is a port to python which I will be using for forming the data to the charts. I don't want to get dirty fingers by creating all this manually forming JSON object using simplejson, especially because there is not complete reference of Open-Flash-Chart API (just examples and tutorials). So I downloaded python port, extracted the content and copied OpenFlashChart.py to climate_change_env/lib/python2.5/site-packages/ which is the location of python libraries which will be installed a.k.a. under python path for the virtual environment "climate_change_env".

Using the examples found at a tutorial, I created a view for providing chart data. The views.py looks now like this:

# -*- coding: UTF-8 -*-
from datetime import datetime, timedelta
from random import random

# http://open-flash-chart-python.googlecode.com/files/Python%202.10.zip
from OpenFlashChart import Chart

from django.db import models
from django.http import HttpResponse
from django.template import RequestContext
from django.shortcuts import get_object_or_404, render_to_response
from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode

Location = models.get_model("climate_change", "Location")
WeatherLog = models.get_model("climate_change", "WeatherLog")

def display_charts(request, slug):
location = get_object_or_404(Location, slug=slug)
return render_to_response(
"climate_change/charts.html",
{'location': location},
context_instance=RequestContext(request),
)

def rgb_to_html_color(r, g, b):
""" convert an R, G, B to #RRGGBB """
return '#%02x%02x%02x' % (r, g, b)

def json_get_statistics(request, slug, field):
location = get_object_or_404(Location, slug=slug)
extreme_values = location.weatherlog_set.aggregate(
models.Min(field),
models.Max(field),
models.Min("timestamp"),
models.Max("timestamp"),
)
elements = []

for year in range(
extreme_values['timestamp__min'].year,
extreme_values['timestamp__max'].year + 1,
):
element = Chart()
monthly_values = []
for month in range (1, 13):
monthly = location.weatherlog_set.filter(
timestamp__year=year,
timestamp__month=month,
).aggregate(models.Avg(field))
monthly_values.append(monthly['%s__avg' % field])
element.values = monthly_values
element.type = "line"
element.dot_style.type = "dot"
element.dot_style.dot_size = 5
random_html_color = rgb_to_html_color(
r=63+int(random() * 128),
g=63+int(random() * 128),
b=63+int(random() * 128),
)
element.dot_style.colour = random_html_color
element.width = 4
element.colour = random_html_color
element.text = str(year)
element.font_size = 10
elements.append(element)

# Create chart
chart = Chart()
chart.y_axis.min = float(str(extreme_values['%s__min' % field]))
chart.y_axis.max = float(str(extreme_values['%s__max' % field]))
chart.y_axis.font_size = 10
chart.title.text = force_unicode(WeatherLog._meta.get_field(field).verbose_name)
chart.x_axis.labels.labels = [
_("Jan"), _("Feb"), _("Mar"), _("Apr"),
_("May"), _("Jun"), _("Jul"), _("Aug"),
_("Sep"), _("Oct"), _("Nov"), _("Dec"),
]

#
# here we add our data sets to the chart:
#
chart.elements = elements

return HttpResponse(chart.create())


If I run the webserver again and go to http://127.0.0.1:8000/climate-change/berlin/, I see a single dot in each graph.



We need some more data. There is Time Machine app in the Applications directory in my computer, so let's use it to move forward to year 2012 to check weather statistics. Damn it! It didn't function properly. Then I'll need to generate statistical data myself. Anyway, according to Mark Twain, there are three kinds of lies: lies, damned lies, and statistics.

I'll use python interactive development environment a.k.a. shell to prepopulate that data.


python manage.py shell
>>> from django.db import models
>>> Location = models.get_model("climate_change", "Location")
>>> location = Location.objects.get(sysname="berlin")
>>> extremes = (
... (-5, 4), (-4, 5), (-2, 10), (2, 15),
... (6, 20), (9, 24), (11, 25), (10, 25),
... (7, 20), (4, 15), (0, 9), (-2, 5),
... )
>>> from random import random
>>> from datetime import datetime
>>> for year in range(2009, 2012):
... for month in range(1, 13):
... temp_range = extremes[month-1][1] - extremes[month-1][0]
... t = extremes[month-1][0] + int(random() * temp_range)
... location.weatherlog_set.create(
... timestamp=datetime(year, month, 1),
... temperature=t,
... humidity=int(random()*100),
... wind_speed="%.2f" % (random()*372),,
... visibility="%.2f" % (random()*100),
... )

I defined minimal and maximal temperatures for each month in a tuple of tuples. Then I went through three years and for each month of that year I added random temperatures which would be in the range of minimal and maximal temperatures for that month. Humidity, wind speed and visibility are randomized without monthly ranges. The graph after data population looks like this:



That's it. Finally, I completed this tutorial. You had a chance to watch the process of Django app development. You saw, how virtualmin, migrations and third-party libraries can be used. Hopefully, you got some inspirations to create something cool by yourself just for fun.

I will publish the full code soon somewhere and you'll have a chance to download it and modify.

If something is too confusing, you don't understand some part of the tutorial, or you think that it could be improved somehow, don't hesitate to write comments.

2009-10-17

Weather App Tutorial. Part 4 of 5. Template Tag

If you followed the first parts of the tutorial, you should have basic understanding how to create an app with models, set up administration, and retrieve data from third-party services. This part is about displaying collected data in any template using custom template tag.

At first, we need to create a directory templatetags containing an empty __init__.py file in the climate_change directory.


mkdir -p climate_change/templatetags
touch climate_change/templatetags/__init__.py


I will call the template library weather. So I have to create a file weather.py in climate_change/templatetags and define and register the template tag in that file. The template tag get_current_weather should display the current weather for a chosen location. To define what location you choose, you could refer to id, name or location_id, but none of them is appropriate for this reason. id and location_id are not remember-able and not informative enough, whereas the name might be changed to translate the city to another language or to add some more specifics and this change would detach the template tag from the location. For those reasons, it is best to create a new field sysname for the location model which would have a unique non-changeable value as a textual humanized identifier for templates.

But wait! It's such a pain to add new fields and modify database schema... Not, if you are using south! Let's quickly install it and then put it under INSTALLED_APPS.

easy_install south



#...
INSTALLED_APPS = (
# django core
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.admin",
# third-party
"south",
# project-specific
"climate_change",
)
#...


Note that south was installed in the virtual environment not spoiling the global python namespace. Now we will syncronize the database to create south_history table, create the initial migration for climate_change and apply it.


# create the missing database table from south app
python manage.py syncdb
# create initial migration for climate_change app which will be used by new projects
python manage.py startmigration climate_change --initial
# fake this migration for this project
python manage.py migrate climate_change --fake


Now we can finally add the new sysname field, create migration for it and apply it.


#...
class Location(models.Model):
sysname = models.SlugField(
_("system name"),
max_length=200,
unique=True,
blank=True,
default="",
help_text=_("Do not change this value"),
)
name = models.CharField(_("name"), max_length=200)
location_id = models.CharField(
_("location ID"),
max_length=20,
help_text=_("Location IDs can be retrieved from URLs of weather "
"at specific cities at Yahoo! Weather, e.g. GMXX0008 from "
"http://weather.yahoo.com/forecast/GMXX0008.html"),
)
#...



# create a new migration called "add_sysname"
python manage.py startmigration climate_change add_sysname --auto
# apply it to the database
python manage.py migrate climate_change


I had to set the default value to empty string because otherwise south throws exception when I use sqlite3. Anyway, after running those commands, I started the built in webserver again and added the sysname "berlin" to the record of Berlin's location.

We can get back to weather.py and add the template tag there


# -*- coding: UTF-8 -*-
from django.db import models
from django import template
from django.template import loader

register = template.Library()

### TAGS ###

def do_get_current_weather(parser, token):
"""
Returns the latest known weather information.

Usage::

{% get_current_weather in <location_sysname> [using <template_path>] [as <var_name>] %}

Examples::

{% get_current_weather in "berlin" using "climate_change/custom_weather.html" %}

{% get_current_weather in "london" as current_weather %}
var sCurrentWeather = "{{ current_weather|escapejs }}";

"""
bits = token.split_contents()
tag_name = bits.pop(0)
template_path = ""
var_name = ""
location_sysname = ""
try:
while bits:
first_word = bits.pop(0)
second_word = bits.pop(0)
if first_word == "in":
location_sysname = second_word
elif first_word == "using":
template_path = second_word
elif first_word == "as":
var_name = second_word

except ValueError:
raise template.TemplateSyntaxError, "get_current_weather tag requires a following syntax: {% get_current_weather [using <template_path>] [as <var_name>] %}"
return CurrentWeatherNode(tag_name, location_sysname, template_path, var_name)

class CurrentWeatherNode(template.Node):
def __init__(self, tag_name, location_sysname, template_path, var_name):
self.tag_name = tag_name
self.location_sysname = location_sysname
self.template_path = template_path
self.var_name = var_name
def render(self, context):
location_sysname = template.resolve_variable(
self.location_sysname,
context,
)
template_path = ""
if self.template_path:
template_path = template.resolve_variable(
self.template_path,
context,
)
context.push()
WeatherLog = models.get_model("climate_change", "WeatherLog")
logs = WeatherLog.objects.filter(
location__sysname=location_sysname,
).order_by("-timestamp")
if logs:
context['weather'] = logs[0]
output = loader.render_to_string(
[template_path, "climate_change/current_weather.html"],
context,
)
context.pop()
if self.var_name:
context[self.var_name] = output
return ""
else:
return output


register.tag("get_current_weather", do_get_current_weather)

### FILTERS ###

# none at the moment


As you might see from the code, the template tag is using a template which can be redefined by the template designer. We still need the default template itself, so I will create a directory templates/climate_change and a file current_weather.html with this content:


{% load i18n %}
<div class="current-wheather">
<h3>{{ weather.location.name }}</h3>
<dl>
<dt>{% trans "Temperature" %}:</dt>
<dd>{{ weather.temperature }}° C</dd>
<dt>{% trans "Humidity" %}:</dt>
<dd>{{ weather.humidity }} %</dd>
<dt>{% trans "Wind speed" %}:</dt>
<dd>{{ weather.wind_speed }} km/h</dd>
<dt>{% trans "Visibility" %}:</dt>
<dd>{{ weather.visibility }} km</dd>
</dl>
</div>


How can I test the template tag? I will need a new page which will include it. So I will add a rule in urls.py to redirect root url to index.html which will extend from base.html.

So the base.html looks like this:


{% block doctype %}>!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">{% endblock %}

{% load i18n %}

<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>{% block title %}simple document{% endblock %}</title>
{% block extra_head %}{% endblock %}
</head>
<body>
<div id="header">{% block header %}{% endblock %}</div>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">{% block footer %}{% endblock %}</div>
</body>
</html>


The index.html looks like this:


{% extends "base.html" %}
{% load i18n weather %}
{% block content %}
{% get_current_weather in "berlin" %}
{% endblock %}


And also we'll need an extension in the urls.py:


from django.conf.urls.defaults import *
from django.contrib import admin

admin.autodiscover()

urlpatterns = patterns("",
(
r"^$",
"django.views.generic.simple.direct_to_template",
{'template': "index.html"},
),
(r"^admin/", include(admin.site.urls)),
)


When I run the development server and go to http://127.0.0.1:8000/, I see this:



It's time for the graphs! The end of this tutorial will be published here tomorrow.

2009-10-16

Weather App Tutorial. Part 3 of 5. Management Command

As you might have noticed, this is a continuous tutorial about an app which regularly checks weather and reports how it changes throughout years. Previously I wrote how to start a project and define the models. The interestingness of the app starts now. I will show you how to import weather details from Yahoo! Weather using a management command.

Management commands in Django are those which are recognized by manage.py and django-admin.py, e.g. syncdb, shell, runserver, etc. It's not difficult to inject your own custom commands for your app. We'll need one that will be set as a scheduled task to check the current weather.

To create a management command, you have to create directories management/commands/ inside of your climate_change directory and create empty files __init__.py in each of them.


mkdir -p climate_change/management/commands
touch climate_change/management/__init__.py
touch climate_change/management/commands/__init__.py

Now I'll create a file climate_change/management/commands/check_weather.py which will aggregate the data from the feed. Just looking at the examples of Django core commands, you can find, that management-command modules should have a class Command with an overridden method handle_norargs which will have all the logic of the command. I found an example of importing weather from Yahoo! and will be using it here.


# -*- coding: UTF-8 -*-
import urllib
from xml.dom import minidom
from pprint import pprint
from datetime import datetime

from django.db import models
from django.core.management.base import NoArgsCommand

Location = models.get_model("climate_change", "Location")
WeatherLog = models.get_model("climate_change", "WeatherLog")

SILENT, NORMAL, VERBOSE = 0, 1, 2

WEATHER_URL = 'http://xml.weather.yahoo.com/forecastrss?p=%s&u=c'
WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0'

def weather_for_location(location_id):
    # taken from http://developer.yahoo.com/python/python-xml.html
    # and modified a little
    url = WEATHER_URL % location_id
    dom = minidom.parse(urllib.urlopen(url))
    forecasts = []
    for node in dom.getElementsByTagNameNS(WEATHER_NS, 'forecast'):
        forecasts.append({
            'date': node.getAttribute('date'),
            'low': node.getAttribute('low'),
            'high': node.getAttribute('high'),
            'condition': node.getAttribute('text')
        })
    ycondition = dom.getElementsByTagNameNS(WEATHER_NS, 'condition')[0]
    ywind = dom.getElementsByTagNameNS(WEATHER_NS, 'wind')[0]
    yatmosphere = dom.getElementsByTagNameNS(WEATHER_NS, 'atmosphere')[0]
    return {
        'current_condition': ycondition.getAttribute('text'),
        'current_temp': ycondition.getAttribute('temp'),
        'current_humidity': yatmosphere.getAttribute('humidity'),
        'current_visibility': yatmosphere.getAttribute('visibility'),
        'current_wind_speed': ywind.getAttribute('speed'),
        'forecasts': forecasts,
        'title': dom.getElementsByTagName('title')[0].firstChild.data,
        'guid': dom.getElementsByTagName('guid')[0].firstChild.data,
    }            

class Command(NoArgsCommand):
    help = "Aggregates data from weather feed"
    def handle_noargs(self, **options):
        verbosity = int(options.get('verbosity', NORMAL))
        
        created_count = 0
        for l in Location.objects.all():
            weather = weather_for_location(l.location_id)
            if verbosity > NORMAL:
                pprint(weather)
            timestamp_parts = map(int, weather['guid'].split("_")[1:-1])
            timestamp = datetime(*timestamp_parts)
            log, created = WeatherLog.objects.get_or_create(
                 location=l,
                 timestamp=timestamp,
                 defaults={
                    'temperature': weather['current_temp'],
                    'humidity': weather['current_humidity'],
                    'wind_speed': weather['current_wind_speed'],
                    'visibility': weather['current_visibility'],
                    }
                 )
            if created:
                created_count += 1
        if verbosity > NORMAL:
            print "New weather logs: %d" % created_count

Now we can try running the command from the project directory using the following:


python manage.py check_weather --verbosity=2

I get this result:

{'current_condition': u'Mostly Cloudy',
 'current_humidity': u'94',
 'current_temp': u'10',
 'current_visibility': u'9.99',
 'current_wind_speed': u'16.09',
 'forecasts': [{'condition': u'Partly Cloudy',
                'date': u'6 Oct 2009',
                'high': u'19',
                'low': u'14'},
               {'condition': u'PM Light Rain',
                'date': u'7 Oct 2009',
                'high': u'23',
                'low': u'14'}],
 'guid': u'GMXX0008_2009_10_06_10_20_CEST',
 'title': u'Yahoo! Weather - Berlin/Schonefeld, GM'}
New weather logs: 1

To define this task as a cron job running hourly, you can either type

echo 0 * * * * cd /path/to/climate_change_env/ && source bin/activate && cd blog_action_day_2009 && python manage.py check_weather && deactivate > crontab.txt
crontab crontab.txt
del crontab.txt
.. or use django-poormanscron app and set the check_weather command there.

OK. Cool. The next thing to do is the template tag which displays the details of recent weather.