Sunday, September 25, 2011

Example #23 - Creating a simple web app

Let's take the code from Example 19 and turn it into a web app.  This will be a trivial example that can be the starting point for building a full web application.

Example 19 used the command line to run a Python program.  One of the program's arguments was a street address.  The program ran and displayed a map in your browser of nearby rat sightings.  In this example, you can enter the street address into a form on a web page and the same sort of map will be displayed.

Here's what it looks like.  You type in the street address and click "Search".



The map is then displayed with the address you entered shown as the blue 'C' marker that denotes the center of the map and the rat sightings (ie., the 311 requests for rodent baitings) shown as red 'R' markers.



Django
I created this using the Django framework.  Django is a platform that supports Python web applications.  You can read about it at the Django project site.

The example runs on my laptop using the free Windows DjangoStack for Windows from BitNami.  This provides the web server, database, and django environment to build apps and test them on your own system.

For production use, you could take the code after it has been tested out on your system and move it to a web hosting provider that runs Django.  I have not done that.  I just wanted to get something simple running on Django.

There is a lot of information on the web about using Django.  The two resources that were most helpful to me were:
  1. Django Project's, Tutorial for writing your first Django app
  2. Django Project's, The Django Book, esp. Chapter 7 on forms

Installation
Follow BitNami's installation instructions.

I installed MySQL but database choice really doesn't matter.  The application does not use any tables. 

I called my project, "Rats."  I then called the only application for the project, "rats."  I didn't understand the naming convention.  It might have made more sense to call the project something like, "opendata," and the application, "rats."

After installing, the directory for the Rats project was:
C:\Documents and Settings\Administrator\BitNami DjangoStack projects\Rats

The contents of the Rats project directory:
09/23/2011  04:08 PM               517 manage.py
09/25/2011  06:19 PM    <DIR>          rats
09/25/2011  04:51 PM             5,586 settings.py
09/25/2011  04:51 PM             3,086 settings.pyc
09/25/2011  06:18 PM               199 urls.py
09/25/2011  06:19 PM               479 urls.pyc
09/23/2011  04:08 PM                 0 __init__.py
09/23/2011  05:32 PM               172 __init__.pyc


The directory for the rats application:
C:\Documents and Settings\Administrator\BitNami DjangoStack projects\Rats\rats

The contents of the rats application directory:
09/23/2011  08:31 PM                60 models.py
09/23/2011  08:34 PM               231 models.pyc
09/25/2011  05:47 PM             7,066 ratslib.py
09/25/2011  05:48 PM             3,815 ratslib.pyc
09/25/2011  05:19 PM    <DIR>          templates
09/23/2011  08:31 PM               399 tests.py
09/25/2011  06:17 PM               565 views.py
09/25/2011  06:17 PM             1,143 views.pyc
09/23/2011  08:31 PM                 0 __init__.py
09/23/2011  08:34 PM               177 __init__.pyc

The template under the rats directory has one html file:
09/25/2011  05:19 PM               274 search_form.html


Files
Following are the contents of the files for this application.  Details about what they do and how they fit together can be found in the two references listed above.  I'll provide a brief overview after listing the files.

settings.py
I made a few changes to ../Rats/settings.py
  1. Set the location for my urlconf file:
    ROOT_URLCONF = 'Rats.urls' 
  2. Added my app to the list of installed apps:
    INSTALLED_APPS = (
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.sites',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'rats',                 
  3. Added my template directory for my html file(s):
    TEMPLATE_DIRS = (
    DjangoStack projects\Rats\rats\templates',
    )
urls.py
from django.conf.urls.defaults import patterns, include, url

urlpatterns = patterns('',
                (r'^search/$', 'rats.views.search'),
                (r'^map/$',    'rats.views.map')


views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from ratslib import geocode, getRatLocs, createMapUrl

def search(request):
    return render_to_response('search_form.html')

def map(request):
    if 'q' in request.GET:
        err1, lat, lng = geocode( request.GET['q'], 'Chicago', 'IL')
        err2, locs = getRatLocs(lat, lng, '100')
        url = createMapUrl(lat, lng, locs, 20)
        return HttpResponseRedirect(url)
    else:
        return HttpResponse('You submitted an empty form.'


search_form.html
<html>
        <head>
            <title>Search for Rats</title>
        </head>
    <body>
        <form action="/map/" method="get">
            <input type="text" name="q">
            <input type="submit" value="Search">
        </form>
    </body>
</html>


Other files
models.py - No changes.  There are no models (database tables) needed for this application.
tests.py - No changes
manage.py - No changes. 
ratslib.py - this contains functions used in views.py.  It is a module holding the geocode(), getRatLocs(), and createMapUrl() functions from Example 19.  See that example to understand the code.  Because it's lengthy and not really germane to how to use Django, you'll find the contents of this file at the bottom of this entry.



Overview
So, how's this all work?
  1. When you start up Django from the Rats directory with python manage.py runserver, it gets the config settings from settings.py. Now, it knows there's a rats app (in the subdirectory, rats), where the urlconf is located, and where to find the html template files.
  2. Go to a browser and use the URL localhost:8000/search. Django finds out from urls.py to call the function search() in views.py.
  3. The search(request) function in views.py runs. Django goes to the template directory and opens search_form.html and sends it to the browser. This file has the HTML for the simplest query form you could have:  one text entry box and a submit button.  When search() finishes, the browser looks like the first screenshot above.  The user will enter a street address and click the Submit button.  The HTML generates a URL: localhost:8000/map, as a GET request. The street address, entered by the user, will be tacked on to the end of the URL in a name/value pair with the name of q  and the value being the street address.
  4. The Django server now gets the URL, localhost:8000/map/q?<street address>. Django looks in urls.py and finds it should call map(request) in views.py.
  5. The map(request) function in views.py runs. It checks that there is a 'q' in the URL string. If not, the user clicked Submit without entering a street address so give an error message. Now, use the code in the ratslib to do the hard work. First, pass in the address to geocode().  The address is the value from the GET request associated with the name of q.  After geocoding the street address, use the lat/lng to query for the rat locations, and build the URL for the static map.  Finally, call HttpResponseRedirect(url). By redirecting to that URL, Google will generate the map and paint it in the browser.
Comments
This example is just the basics for web application doing a query using SODA.  You can take this in a lot of different directions:
  • By changing getRatLocs(), you could query other views that have locations.  For example, you could search for graffiti, crime, or fallen tree limbs.  You'll need to set the right values for hostName, originalViewId, columnId, colNumLat, and colNumLng.
  • Use a Google dynamic map instead of a static map.  The dynamic map will let the user zoom in and out.
  • Use paging to bring back, say 20, rats at a time and place on the map.
  • Add an entry field or radio buttons on the form for the radius of the circle.  The user could enter the street address and enter or select the size of the circle to search.
The example left out considerations for security, performance, user experience, and best django coding techniques.  There's work needed in all these areas before making it a public app.


ratslib.py

import httplib
import json
import urllib
from urllib2 import Request, urlopen, URLError, HTTPError

#
#   geocode(street, city, state) returns the latitude and longitude of a
#       street address using Google's map API.  Note, there is a limit on
#       the number of geocode requests you can make each day to Google.
#
#   Args:
#       street - string - street address
#       city - string - city name
#       state - string - state name
#
#   Returns:
#       err - string - error information.  If empty, geocode was successful
#       lat - string - latitude of the street address
#       lng - string - longitude of the street address
#
def geocode(street, city, state):
    err = ""
    lat = ""
    lng = ""
    url =  "http://maps.googleapis.com/maps/api/geocode/json?address=" + \
        urllib.quote_plus(street) + ',' + \
        urllib.quote_plus(city)   + ',' + \
        urllib.quote_plus(state)  + \
        "&sensor=false"
    req = Request(url)
    try:
    u = urlopen(req)
    except URLError, e:
        if hasattr(e, 'reason'):
            err = e.reason
    elif hasattr(e, 'code'):
            err = e.code
    else:
        response = json.load(u)
        # Check that Google sent back valid data.  If so, get lat and long.
        if response['status'] == 'OK':
            lat = response['results'][0]['geometry']['location']['lat']
            lng = response['results'][0]['geometry']['location']['lng']
        # otherwise, Google returned an error
        else:
            err = 'Google error code: %s\n' % response['status']
    return err, str(lat), str(lng)

#
#   getRatLocs(lat, lng, rad) returns a list of the locations of rats
#       from the Chicago 311 service requests for rodent baiting.  The
#       locations will be within the circle that has a center point at
#       lat/lon and a radius of rad meters.
#
#   Args:
#       lat - string - latitude of the center point of the circle
#       lng - string - longitude of the center point of the circle
#       rad - string - radius, in meters, of the circle
#
#   Returns:
#       err - string - error information.  If empty, geocode was successful
#       locs - list - each item being a sublist containing two elements:
#           latitude and longitude that represents the location of a rat
#           baiting request.
#
def getRatLocs(lat, lng, rad):
    # parameters used in the SODA POST request to do the search
    hostName   = "data.cityofchicago.org"
    service    = "/views/INLINE/rows"
    formatType = "json"
    parameters = "method=index"
    headers    = { "Content-type:" : "application/json" }
    # SODA inline query.  Lat, Lng, Radius are set to 0 here.
    query = {
        "originalViewId": "97t6-zrhs",
        "name": "Nearby rats",
        "query": {
            "filterCondition": {
                "type": "operator",
                "value": "within_circle",
                "children":
                    [
                        {
                            "type": "column",
                            "columnId": 2849547
                        },
                        {
                            "type": "literal",
                            "value": 0
                        },
                        {
                            "type": "literal",
                            "value":  0
                        },
                        {
                            "type": "literal",
                            "value": 0
                        }
                    ]
                }
            }
         }
    # constants used to index into inline filter children[]
    queryLat = 1
    queryLng = 2
    queryRad = 3
    # constants for table column numbers in query return data
    colNumLat = 22
    colNumLng = 23
    # constants used to index into locs
    locLat = 0
    locLng = 1

    # initialize return variables to be empty
    err = ''
    locs = list()

    # put lat, lng, radius into the inline query
    query["query"]["filterCondition"]["children"][queryLat]["value"] = lat
    query["query"]["filterCondition"]["children"][queryLng]["value"] = lng
    query["query"]["filterCondition"]["children"][queryRad]["value"] = rad
    # setup and send the inline query
    jsonQuery = json.dumps(query)
    request = service + '.' + formatType + '?' + parameters
    conn = httplib.HTTPConnection(hostName)
    conn.request("POST", request, jsonQuery, headers)
    response = conn.getresponse()
    # check for good response, pull data out of response and setup locs
    if response.reason != 'OK':
        err = "%s %s" % (response.status, response.reason)
    else:
        rawResponse = response.read()
        jsonResponse = json.loads(rawResponse)
        for rowData in jsonResponse['data']:
            locs.append([rowData[colNumLat], rowData[colNumLng]])
    return err, locs


#
#   createMapUrl(centerLat, centerLng, locs) returns the URL for a Google
#       static map that has a marker for the center of the map and markers
#       for the locations in locs.
#
#   Args:
#       centerLat - string - latitude of the center point of the map
#       centerLng - string - longitude of the center point of the map
#       locs - list - where each item is a location to be marked on the map.
#           Each item is a sublist with two strings: latitude and longitude
#       n - integer - the first n locations in locs will be marked on the
#           map.  There is an upper limit on the size of the URL sent to
#           the Google Map API.  A long list of locations in the URL will
#           not be accepted.  n keeps the URL within the limit.
#
#   Returns:
#       URL - string - URL to send to the Google Map API to create a static
#           map in the default browser.
#
def createMapUrl(centerLat, centerLng, locs, n):
    # define constants for indexing into the locs sublist
    locLat = 0
    locLng = 1
    # define parameters for the map
    urlMapAPI = "http://maps.googleapis.com/maps/api/staticmap"
    mapZoomLevel = "14"
    mapHeight = "512"
    mapWidth = "512"
    mapType = "roadmap"
    centerMarkerColor = "blue"
    centerMarkerLabel = "C"
    locMarkerColor = "red"
    locMarkerLabel = "R"

    url = \
        urlMapAPI + '?' + \
        'center=' + centerLat + ',' + centerLng + \
        '&' + \
        'size=' + mapHeight + 'x' + mapWidth + \
        '&' + \
        'maptype=' + mapType + \
        '&' + \
        'sensor=false' + \
        '&' + \
        'markers=color:' + centerMarkerColor + '%7C' + \
        'label:' + centerMarkerLabel + '%7C' + centerLat + ',' + centerLng

    i = 0
    for loc in locs:
        if i < n:
            url += '&' + \
                'markers=color:' + locMarkerColor + '%7C' + \
                'label:' + locMarkerLabel + '%7C' + \
                loc[locLat] + ',' + loc[locLng]
        i +=1
    return url


1 comment:

  1. Terrific short article. All the content has an affect on a large amount of emergency worries of our population. Everyone can not be uninvolved to make sure you all of these worries. This approach content gives you plans and additionally techniques. Particularly revealing and additionally effective.
    http://melhor-hospedagem-de-sites.strikingly.com/

    ReplyDelete