Pylons Quick Site Development

Author: Dave Kuhlman
Address:
dkuhlman@rexx.com
http://www.rexx.com/~dkuhlman
Revision: 1.1a
Date: February 22, 2009
Copyright:Copyright (c) 2007 Dave Kuhlman. All Rights Reserved. This software is subject to the provisions of the MIT License http://www.opensource.org/licenses/mit-license.php.
Abstract:This document attempts to help you to quickly build your first Pylons Web application.

Contents

1   Introduction

There is good documentation at the Pylons Web site. In particular, you may want to work through the example applications at The Path to Mastery.

2   Installation

Follow the instructions at Installing Pylons.

Hopefully, you can install Pylons with the following:

$ easy_install Pylons

2.1   Suggestion -- Protecting your environment

A small word of caution here -- When you use easy_install to install Pylons, a number of different Python packages will be installed on your machine. And, easy_install forces the path to the packages that it installs up higher in sys.path than other packages installed on your system. If you have a specially customized version of one of these packages, your Python will no longer find that version. I had this difficulty with Docutils, which contains an extra output writer ODF/ODT writer for Docutils which I wrote to produce files that can be loaded into OpenOffice Writer and which is not in the standard Docutils distribution. After installing Pylons, Python finds the version of Docutils installed by easy_install instead of the version in which the ODF/ODT writer is installed. If this is an issue for you, then you may have to use a sitecustomize.py module or other technique in order to use your version of that module from Python.

easy_install, in a sense, hijacks your Python environment.

If you are concerned about this, you may want to read my notes about easy_install and workingenv.py: sys.path, PYTHONPATH, site.py, contd..

2.2   Templating languages -- Your choice

Pylons allows you to choose which of several templating languages is used in your application. The default is Myghty. However, it appears that there is movement toward Mako. Also consider:

If you decide to use a templating language other than the default (currently Myghty), then see Template Language Plugins for instructions.

Also see the Pylons Wiki.

Installing Mako can done with:

$ easy_install Mako

3   Creating an Application

  1. Generate the application skeleton and files -- Python Paste does this for us. Paste was installed when you installed Pylons, if it was not already there.

    Let's create an application called FirstApp, which will be the example used throughout this document. Use the following:

    $ paster create --template=pylons FirstApp
    
  2. Edit the configuration file -- FirstApp/development.ini. For example, you may want to change the port on which your application is served. In my case I changed the port to 5001:

    [server:main]
        o
        o
        o
    port = 5001
    
  3. Start your new application -- You should be able to start your server with the following:

    $ cd FirstApp
    $ paster serve --reload development.ini
    

    Notice the flag --reload. This flag will enable you to make changes in your application, and then to see those changes without having to re-start your server.

    When you start the server, it might look something like this:

    $ paster serve --reload development.ini
    Starting subprocess with file monitor
    /home/dkuhlman/a1/Python/Pylons/PylonsEnv/lib/python2.5/simplejson-1.4-py2.5.egg/simplejson/scanner.py:6: DeprecationWarning: The sre module is deprecated, please import re.
      from sre import VERBOSE, MULTILINE, DOTALL
    Starting server in PID 6419.
    serving on 0.0.0.0:5001 view at http://127.0.0.1:5001
    
  4. Interact with your new application -- If, in development.ini you changed the port to 5001, for example, then you should be able to view a default page in your Web browser at http://localhost:5001/.

4   Creating a Controller

Now we will create a controller and map a URL to a method in the conroller, so that when that URL is requested, our method in the controller will be called. The controller tells our application what to do in response to each request. A controller is the module and class that handles requests from your clients.

  1. Create the controller -- We will create a controller using Python Paste. Something like the following should do it:

    $ cd FirstApp
    $ paster controller firstcontroller
    

    This creates a new module firstapp/controllers/firstcontroller.py.

  2. Map a request to the controller -- In order to do this, we will edit the file firstapp/config/routing.py. Add a line like the following:

    map.connect('mapping1', '/firstapp',
        controller='firstcontroller', action='index' )
    

    This maps the URL http://localhost:5004/firstapp to the action/handler index.

    See the Routes documentation and Adding More Complex Mappings below for more on this.

  3. Create the handler (action) in the controller -- Our controller is firstcontroller and our action is index. So, in controllers/firstcontroller.py, we add a method named index. It might look something like this:

    class FirstcontrollerController(BaseController):
        def index(self):
            return Response('<p>firstapp default</p>')
    

Now refresh the Web page http://localhost:5004/firstapp and you should see:

firstapp default

And if it did not work:

5   Creating Content for your Application

To do this, map the URL http://localhost:5004/firstapp/test1 to the action test1. In firstapp/config/routing.py, add:

map.connect('mapping2', '/firstapp/test1',
    controller='firstcontroller', action='test1' )

In firstapp/controllers/firstcontroller.py, add:

def test1(self):
    return render_response('/firstapp/test1.myt')

This tells our application to produce content by calling the action/handler test1 in our controller firstcontroller. And, that test1 method in turn, generates the content by processing the file firstapp/templates/firstapp/test1.myt with Myghty (since we have not changed the default template plugin).

Next, create firstapp/templates/firstapp/test1.myt, containing something like the following:

<html>
<head>
<title>Test #1</title>
</head>
<body>
<h1>Test #1</h1>
<%python>
items = ['one', 'two', 'three', 'four', ]
</%python>
<ol>
% for item in items:
    <li>Item <% item.capitalize() %></li>
% # end for
</ol>
</body>
</html>

The above template contains Myghty code. For the present, it may be enough to understand:

See the Myghty Documentation for help with learning how to write this kind of content.

5.1   Passing values to the template

Although you can include Python code in your Myghty (or other) template, you will often want to place application logic in your controller, and then pass computed values to your template. Here's how. I'll map another URL to a new action as an example.

In firstapp/config/routing.py, add:

map.connect('mapping3', '/firstapp/test2',
    controller='firstcontroller', action='test2' )

In firstapp/controllers/firstcontroller.py, add:

def test2(self):
    random1 = random.randint(1, 10)
    random2 = random.randint(1, 10)
    random3 = random.randint(1, 10)
    random4 = random.randint(1, 10)
    c.random_values = [random1, random2, random3, random4, ]
    return render_response('/firstapp/test2.myt')

Explanation:

  • The variable c -- c is what we use to pass values to our template for one time use. The c variable is reset on each request. See Controller Variables and Template Globals for more explanation.
  • The variable g -- Had we wanted to pass values that would be preserved across multiple requests, we would use the variable g instead. However, the variable g is shared across all requests, which is usually not what you want. See the section on Sessions below for information about how to share values across responses within a single session.
  • The variable h -- h contains helper functions, for example from the Web Helpers package. See section Using Webhelpers to create forms for more on using helpers in forms.

And, now add a template to use those values, specifically, firstapp/templates/firstapp/test2.myt:

<html>
<head>
<title>Test #2</title>
</head>
<body>
<h1>Test #2</h1>
<ol>
% for item in c.random_values:
    <li> <% item %></li>
% # end for
</ol>
</body>
</html>

Explanation:

  • Notice that, using the variable c, we access c.random_values that we set in the controller.

6   Adding More Complex Mappings

The underlying technology used by Pylons to map URLs to actions is Routes. See the Routes Manual.

We have already seen how to add a simple mapping that routes a fixed URL to a specific action (method). In this section we will learn two techniques that make that mapping more flexible.

6.1   Wildcard paths

Here is an example.

  1. First, a mapping in in firstapp/config/routing.py:

    map.connect('mapping4', '/firstapp/test4/*category/help',
        controller='firstcontroller', action='test4')
    
  2. Next, the action/method in firstapp/controllers/firstcontroller.py:

    def test4(self):
        return render_response('/firstapp/test4.myt')
    
  3. And, some trivial content in firstapp/templates/firstapp/test4.myt:

    <html>
    <head>
    <title>Test #4</title>
    </head>
    <body>
    <h1>Test #4</h1>
    <p>Hello.  You want help, right?</p>
    </body>
    </html>
    

Caution: Be aware that problems can occur when mixing wildcard parts with dynamic parts. See Wildcard Limitations and Gotchas for more on this.

6.2   Dynamic paths

A dynamic path is Routes terminology for the ability to assign a part of a path (in a URL) to a variable. Let's consider an example:

  1. First, add the mapping in firstapp/config/routing.py:

    map.connect('mapping3', '/firstapp/test3/:userid',
        controller='firstcontroller', action='test3',
        userid='[nobody]' )
    

    Notice the colon in the path. The value received in that part of the path is assigned, in this case, to the variable userid. Also notice that the variable userid is given a default value "[nobody]".

  2. Next, add the action in firstapp/controllers/firstcontroller.py:

    def test3(self, userid):
        c.userid = userid
        return render_response('/firstapp/test3.myt')
    
  3. And now, add some content in firstapp/templates/firstapp/test3.myt:

    <html>
    <head>
    <title>Test #3</title>
    </head>
    <body>
    <h1>Test #3</h1>
    <p>Hello user <% c.userid %>.</p>
    </body>
    </html>
    

Notice that you can have more than one dynamic part in your URL specification. For example:

map.connect('mapping3', '/firstapp/test3/:userid/:username',
    controller='firstcontroller', action='test3',
    userid='[nobody]', username='[noname]' )

And, we can capture these values in our action with something like the following:

def test3(self, userid, username):
    c.userid = userid
    c.username = username
    return render_response('/firstapp/test3.myt')

7   Add a Form and Retrieve Values From the Form

This section explains two methods of creating HTML forms for Pylons: (1) a simple, manual way and (2) the Web Helpers way.

7.1   Using simple, manual forms

  1. First, we add a mapping to firstapp/config/routing.py:

    map.connect('mapping5', '/firstapp/test5/:itemnumber/:color',
        controller='firstcontroller', action='test5',
        itemnumber='[void]', color='[default]')
    
  2. Next, add an action in firstapp/controllers/firstcontroller.py:

    def test5(self, itemnumber, color):
        if 'itemnumber' in request.params and 'color' in request.params:
            c.itemnumber = request.params['itemnumber']
            c.color = request.params['color']
        else:
            c.itemnumber = itemnumber
            c.color = color
        return render_response('/firstapp/test5.myt')
    

    Our action simply feeds values back to the form. If values have been submitted from the form, the action feeds those back, and if not, it feeds in values (possibly defaults from the mapping) from dynamic parts in the URL.

  3. Now, the content, including the form in firstapp/templates/firstapp/test5.myt:

    <html>
    <head>
    <title>Test #5</title>
    </head>
    <body>
    <form name="test5form" action="/firstapp/test5" method="PUT">
    <h1>Test #5</h1>
    <p>Your current item number is <% c.itemnumber %> and your
    current color is <% c.color %>.
    </p>
    <p>Item number:
    <input type="text" name="itemnumber" value=<% c.itemnumber %> />
    </p>
    <p>Color:
    <input type="text" name="color" value=<% c.color %> />
    </p>
    <p>
    <input type="submit" name="submit" value="submit" />
    </p>
    </form>
    </body>
    </html>
    

A few notes:

  • The action in the form element causes this page, when submitted, to request this page again.
  • Myghty value replacements (for example, value=<% c.itemnumber %>), insert previous values, captured by the controller, into the form.

7.2   Using Webhelpers to create forms

Here we use Webhelpers in creating our form. You can learn more about these and other helpers available in Pylons applications at Web Helpers.

  1. Add the mapping to firstapp/config/routing.py:

    map.connect('mapping6', '/firstapp/test6/:itemnumber/:color',
        controller='firstcontroller', action='test6',
        itemnumber='[void]', color='[default]')
    
  2. Add an action in firstapp/controllers/firstcontroller.py:

    def test6(self, itemnumber, color):
        if 'itemnumber' in request.params and 'color' in request.params:
            c.itemnumber = request.params['itemnumber']
            c.color = request.params['color']
        else:
            c.itemnumber = itemnumber
            c.color = color
        return render_response('/firstapp/test6.myt')
    

    It is almost the same as the previous example.

  3. Add the content in firstapp/templates/firstapp/test6.myt:

    <html>
    <head>
    <title>Test #6</title>
    </head>
    <body>
    <h1>Test #6</h1>
    <p>Your current item number is <% c.itemnumber %> and your
    current color is <% c.color %>.
    </p>
    <% h.form(h.url(action='test6'), method='put') %>
    <fieldset>
    <table border="0" width="50%">
    <tr>
    <td width="50%"><label for="itemnumber">Item number:</label></td>
    <td width="50%"><% h.text_field('itemnumber', value=c.itemnumber) %></td>
    </tr>
    <tr>
    <td width="50%"><label for="color">Color:</label></td>
    <td width="50%"><% h.text_field('color', value=c.color) %></td>
    </tr>
    <tr>
    <td colspan="2" align="center"><% h.submit('Submit') %></td>
    </tr>
    </table>
    </fieldset>
    <% h.end_form() %>
    </form>
    </body>
    </html>
    

Notes:

  • Webhelpers are available in the global varialbe h.
  • We use h.form() and h.end_form() to begin and end our form.
  • Notice the use of h.url() to obtain a URL for our action.
  • In our example, we use h.text_field() to create input text fields and we use h.submit() to create a submit button. Webhelpers also provides functions for the creation of selection boxes, text areas, hidden fields, radio buttons, and check boxes. See Webhelpers Form Tag Helpers.

8   Accessing Configuration Values

First read Project Configuration and Logging -- http://pylonshq.com/docs/en/0.9.7/#project-configuration-and-logging for information on working with Pylons configuration values.

Next, look at Paste Deployment -- http://pythonpaste.org/deploy/ and click on "Config Format" to learn about the format for Pylons configuration files.

When you start your application with something like:

$ paster serve --reload development.ini

you are telling the server to use development.ini as the configuration file.

8.1   Accessing configuration values in your controller

When the request object is available, the configuration object may be obtained as follows:

config = request.environ['paste.config']

8.2   Accessing configuration values elsewhere

Where the request object is not available, you can get the config object with the following:

from paste.deploy import CONFIG
config = CONFIG

8.3   Contents of the config object

The config object is a dictionary like object containing following keys:

  • global_conf -- A dictionary containing key value pairs from the [DEFAULT] section of the configuration file.
  • app_conf -- A dictionary containing key value pairs from the [app:main] section of the configuration file.

8.4   An example

So, for example, if I add the following under the [app:main] section of my configuration file:

decoration.title = A Pretty Heading

Then I can get the value of decoration.title with either:

from paste.deploy import CONFIG
config = CONFIG
app_conf = config['app_conf']
title = app_conf['decoration.title']
description = app_conf['decoration.description']

Or, when the request object is available:

config = request.environ['paste.config']
app_conf = config['app_conf']
title = app_conf['decoration.title']
description = app_conf['decoration.description']

8.5   Adding values to the g global variable

You can also add values to the g globals variable. You do this in lib/app_globals.py. For example, to set a global value named level, add the following to the __init__ method in the Globals class:

self.level = 6

Then in your controller (wherever the g variable is available), you can access that value with, for example:

level = g.level

Or, in your template, with:

<p>My level is: <% g.level %></p>

9   Database Access -- Using a Data Model

We'll use SQLAlchemy, since that seems more in the future. For more information see The Python SQL Toolkit and Object Relational Mapper.

9.1   Configure the use of SQLAlchemy

Uncomment and edit the SQLAlchemy line in FirstApp/development.ini. In my case, I have:

sqlalchemy.dburi = postgres://postgres:xxxx@localhost:5432/test
sqlalchemy.echo_queries = true

Which specifies:

  • User: postgres
  • Password: xxxx
  • Host: localhost
  • Port: 5432
  • Database name: test

Now add the following to firstapp/lib/app_globals.py:

from firstapp.models import init_model

And, add the following to the __init__ method, also in firstapp/lib/app_globals.py:

init_model(app_conf)

This will allow us to make use of app_conf in models/__init__.py in order to get the value for the DSN that we defined in development.ini. In my case, that value is defined in development.ini by the line:

sqlalchemy.dburi = postgres://postgres:mypassword@localhost:5432/test

9.2   Define and implement the model

I'm using PostgreSQL on my machine. Here is what my test database looks like, as shown using the PostgreSQL utility psql:

test=# select * from plant_db;
   p_name   |      p_desc      | p_rating
------------+------------------+----------
 lemon      | yello and tart   |        5
 sunflower  | brite yellow     |        3
 tangerine  | orange and sweet |        7
 grapefruit | pink and sweet   |        7
(4 rows)

So, in FirstApp/firstapp/lib/app_globals.py we add:

from firstapp.models import init_model

near the top. And, in the same file, we add:

init_model(app_conf)

in the __init__() method.

Next, we add the following to FirstApp/firstapp/models/__init__.py:

import sqlalchemy as sa
import sqlalchemy.orm as orm

class Plant(object):
    def __repr__(self):
        return "%s(%r,%r,%r)" % (
            self.__class__.__name__,
            self.p_name,
            self.p_desc,
            self.p_rating,
            )

def init_model(app_conf):
    global metadata, plant_table
    if not globals().get('metadata'):
        dsn = app_conf['sqlalchemy.dburi']
        metadata = sa.BoundMetaData(dsn)
        plant_table = sa.Table('plant_db', metadata, autoload=True)
        orm.mapper(Plant, plant_table)

def create_all(app_conf):
    init_model(app_conf)
    metadata.create_all()

Notes:

  • We access the DSN through the key "sqlalchemy.dburi". That key and its value are defined in my development.ini file.

9.3   Map the URL

To test all this, we'll add a new mapping for a new URL, as well as a new action and Web page (template).

Add the following to firstapp/config/routing.py:

map.connect('mapping7', '/firstapp/test7/:p_name',
    controller='firstcontroller', action='test6',
    p_name='weed')

The :p_name dynamic part of the URL will allow us to pass in a plant name.

9.4   Implement the action

The action (method) is where we will do the database work, for example, the database queries, the Add operation, the Delete operation, and the Update operation.

As a sample, add something like the following to firstapp/controllers/firstcontroller.py:

import sqlalchemy as sa
import sqlalchemy.orm as orm
from firstapp.models import Plant                    # Note 1 (see below)

class FirstcontrollerController(BaseController):
    o
    o
    o
def test7(self, p_name):
    if ('p_name' in request.params and               # Note 2
        'p_desc' in request.params and
        'p_rating' in request.params):
        c.p_name = request.params['p_name']
        c.p_desc = request.params['p_desc']
        c.p_rating = request.params['p_rating']
    else:
        c.p_name = p_name
        c.p_desc = '[void]'
        c.p_rating = '[void]'
    dbsession = orm.create_session()                 # Note 3
    if 'commit' in request.params:
        if request.params['commit'] == 'Add':        # Note 4
            query = dbsession.query(Plant)
            plant = query.get_by(p_name=c.p_name)
            if plant is None:
                plant = Plant()
                plant.p_name = c.p_name
                plant.p_desc = c.p_desc
                plant.p_rating = c.p_rating
                dbsession.save(plant)
                dbsession.flush()
        elif request.params['commit'] == 'Delete':   # Note 5
            query = dbsession.query(Plant)
            plant = query.get_by(p_name=c.p_name)
            if plant is not None:
                dbsession.delete(plant)
                dbsession.flush()
        elif request.params['commit'] == 'Update':   # Note 6
            query = dbsession.query(Plant)
            plant = query.get_by(p_name=c.p_name)
            if plant is not None:
                plant.p_desc = c.p_desc
                plant.p_rating = c.p_rating
                dbsession.update(plant)
                dbsession.flush()
        elif request.params['commit'] == 'Show':
            pass
    query = dbsession.query(Plant)                   # Note 7
    c.plants = query.execute('select * from plant_db order by p_name')
    dbsession.close()
    return render_response('/firstapp/test7.myt')

Notes:

  1. We import what we need from sqlalchemy and we import our model class.
  2. Next, we capture the variable values from the command line or from the form.
  3. We create a database session. Note that the database connection and the mapper have already been initialized in FirstApp/firstapp/models/__init__.py (see above).
  4. The Add operation -- If a plant with that name does not exist in the database, then create a new plant object, populate its values, save it, and flush the database.
  5. The Delete operation -- Create a query object, then get the plant to be deleted. Next, if the object/plant exists, delete the object and flush to the database.
  6. The Update operation -- Create a query object and get the existing plant. If it exists, update the values in the plant object, then update and flush to the database.
  7. Query the database and pass the values for all plants found into the form. Then, close the database session, and, finally, render the form.

9.5   Implement a page that uses the DB model

Here is a page (using the Myghty templating language) that displays the results of the database query and enables the user to add, delete, and update rows:

<html>
<head>
<title>Test #7</title>
</head>
<body>
<h1>Test #7 -- Plants Database</h1>
<p>Your current item number is <% c.itemnumber %> and your
current color is <% c.color %>.
</p>

<!-- Show the existing database.
-->
<table border="1" width="100%">
<tr>
<th>Name</th>
<th>Description</th>
<th>Rating</th>
</tr>

% for plant in c.plants:
  <tr>
    <td width="20%"><% plant.p_name %></td>
    <td width="60%"><% plant.p_desc %></td>
    <td width="20%"><% plant.p_rating %></td>
  </tr>
% # end for
</table>

<!-- A form for database entries.
-->
<% h.form(h.url(action='test7'), method='put') %>
<fieldset>
<table border="1" width="50%">
<tr>
<td width="50%"><label for="p_name">Plant name:</label></td>
<td width="50%"><% h.text_field('p_name', value=c.p_name) %></td>
</tr>
<tr>
<td width="50%"><label for="p_desc">Description:</label></td>
<td width="50%"><% h.text_field('p_desc', value=c.p_desc) %></td>
</tr>
<tr>
<td width="50%"><label for="p_rating">Rating:</label></td>
<td width="50%"><% h.text_field('p_rating', value=c.p_rating) %></td>
</tr>
</table>
<table border="1" width="50%">
<tr>
<td align="center"><% h.submit('Add') %></td>
<td align="center"><% h.submit('Delete') %></td>
<td align="center"><% h.submit('Update') %></td>
<td align="center"><% h.submit('Show') %></td>
</tr>
</table>
</fieldset>
<% h.end_form() %>
</form>
</body>
</html>

10   Application Structure

This section attempts to give some help with structuring and organizing the code in your application. It's a "what goes where and why" sort of section.

The first thing to notice is that Pylons already gives quite a bit of guidance about structure. For example:

What follows describes a few options and variations.

  1. All in one controller -- Put all your code in one class in a single controller. Consider implementing extended behavior in helper methods (optionally named with a single leading underscore, which the Python style guide says is the convention for non-public members) or in functions outside the controller class.

  2. One controller plus import -- Use a single controller, but put application logic in separate modules. Then, in your controller, import those modules and use the application code that is in classes or functions in those application modules. Consider putting those additional modules in myapp/lib or myapp/controllers or in a subdirectory that you create under one of those. If you create a subdirectory, be sure to add an __init__.py file in the subdirectory.

  3. Multiple controllers -- Use Python Paste to generate multiple controllers, then use option 1 (all in one controller) or option 2 (one controller plus import) above in each of those modules.

  4. If you need variables, functions, or classes for use in multiple templates, you can put definitions in lib/helpers.py. Those definitions will be available in your templates in the h variable. See the comment in lib/helpers.py, which reads as follows:

    "All names available in this module will be available under the Pylons h object."

  5. If you need global variables that live across multiple request/responses, you can assign values to the variable g. You can also initialize the variables in myapp/lib/app_globals.py.

11   Sessions

Sessions in Pylons are reasonably easy. This section presents a quick and trivial example. The example initializes and increments a counter each time this page is visited. If you visit this page from two separate browsers or restart your browser, you should see two separate counts.

Documentation on the use of sessions in Pylons is available under Internationalization, Sessions, and Caching -- http://pylonshq.com/docs/en/0.9.7/#internationalization-sessions-and-caching.

For our example, we add the following to firstapp/config/routing.py:

map.connect('mapping8', '/firstapp/test8',
    controller='firstcontroller', action='test8')

And, add an action handler something like the following to your controller, in our case controllers/firstcontroller.py:

def test8(self):
    if 'count' in session:
        count = session['count']
    else:
        count = 0
    count += 1
    session['count'] = count
    session.save()
    c.count = count
    response = render_response('/firstapp/test8.myt')
    return response

Then implement a template to show our counter and how it is incremented. Here is a sample templates/firstapp/test8.myt:

<html>
<head>
<title>Test #8</title>
</head>
<body>
<p>Hello, user.</p>
<p>Count: <% c.count %></p>
</body>
</html>

A few additional notes on sessions:

12   A Little Ajax

The Pylons site provides a good first example of the use of Ajax in Pylons. See: Getting started with AJAX -- http://docs.pythonweb.org/display/pylonscookbook/Getting+started+with+AJAX.

This section provides an additional Pylons/Ajax example. In this example, the Web page makes a call back into the server to retrieve a specific value, then sets the value of a form input element with that retrieved value.

12.1   The JavaScript Ajax request and callback function in the Web page

First let's look at some JavaScript. Here is the event that triggers the call to a JavaScript function:

onClick="javascript:set_cell_location(<% idx1+1 %>, <% idx2+1 %>)">

The above code is on a cell (a <td> element) in a table of cells in a Myghty template. The cells are generated using the index variables idx1 and idx2, which correspond to the row and column within the table.

Now lets look at the JavaScript function that is executed when the user clicks on a cell in the table:

<script language ="JavaScript">
function set_cell_location(row, column) {
    var el1;
    el1 = document.getElementById("row_field");
    el1.value = row;
    el1 = document.getElementById("column_field");
    el1.value = column;
    el1 = document.getElementById("value_field");
    set_cell_location1(row, column);
    el1.focus();
}
function set_cell_location1(row, column) {
    var url = '/sudoku/get_possibles';
    var pars = 'row=' + row + '&column=' + column;
    var myAjax = new Ajax.Request(
        url,
        {
            method: 'get',
            parameters: pars,
            onComplete: set_cell_location2
            });
}
function set_cell_location2(request) {
    var xmlDoc = request.responseXML;
    var possible = xmlDoc.getElementsByTagName("possible");
    if (possible[0].childNodes.length > 0) {
        possible = possible[0].childNodes[0].nodeValue;
    } else {
        possible = '';
    } // if
    el1 = document.getElementById("value_field");
    el1.value = "";
    el1.value = possible;
}
</script>

Notes and explanation:

  • When the user clicks on a cell, the JavaScript function set_cell_location is called. It receives the row and column as parameters.

  • set_cell_location sets the values of row_field and column_field to the row and column respectively, then calls set_cell_location1 to make our Ajax call.

  • set_cell_location1 makes the Ajax call. Note the URL and parameters. sudoku is my Pylons controller and get_possibles is the method in that controller (actually in the controller class) that we want to call. The parameters are formatted as URL-encoded arguments on the URL used for the request.

  • Note the onComplete option on the Ajax call. It specifies the the JavaScript call-back function that will get control when get_possibles (on the server) returns.

  • set_cell_location2, when it gets control, retrieves the XML that was returned, as a response, by get_possibles. It extracts a value from that chunk of XML and sets the value of an input field in the HTML form to that value. Note the use of:

    var xmlDoc = request.responseXML;
    

    to retrieve the XML from the server.

12.2   The Ajax request handler on the server

Now let's look at the get_possibles method on the server:

class SudokuController(BaseController):
    o
    o
    o
    def get_possibles(self, row='1', column='1'):
        """Ajax call-back.
        Return XML containing row, column, and value/possible.
        Only return value if there is a single possible.
        """
        if 'row' in request.params:
            row = request.params['row']
        if 'column' in request.params:
            column = request.params['column']
        irow = int(row) - 1
        icolumn = int(column) - 1
        if 'table' in session:
            table = session['table']
            cell = table.get_cell(irow, icolumn)
            possibles = cell.get_possibles()
            possibles = list(possibles)
            if len(possibles) == 1:
                possibles = possibles[0]
            else:
                possibles = 'Select'
        s1 = RESULT1 % (row, column, possibles, )
        response = Response(s1)
        response.headers['content-type'] = 'text/xml'
        return response

RESULT1 = '''\
<result>
  <row>%s</row>
  <column>%s</column>
  <possible>%s</possible>
</result>
'''

Notes and explanation:

  • get_possibles retrieves its input parameters in the same way that any Pylons controller action does, specifically from request.params.
  • It then retrieves the table object (our model; hey, maybe we're doing MVC, i.e. model-view-controller) from the session. Then, it retrieves the cell corresponding to the row and column from the table, after which it retrieves the possible values (a Python set) from the cell.
  • And, finally, it formats a chunk of XML and returns that as the response, after setting the content type to text/xml.

Summary -- It's a little involved, but I think you can see a pattern here:

  1. JavaScript in the Web page on the client that makes an Ajax call to the server.
  2. A method in the controller on the server that (1) retrieves it's parameters from the request.params object and (2) returns it's response as a snippet of XML.
  3. A JavaScript call-back method in the Web page on the client that extracts the needed values from the XML in that response, and then uses those values to update the Web page.

13   A Few Additional Notes

13.1   Debugging

When your Pylons application on the server breaks (throws an exception), you will see a traceback in your Web browser window.

Here are a few things that you can do when you receive a traceback:

  • Click on the full traceback button to get more context and a deeper stack trace.
  • Click on the text version button to get the traceback formatted in the standard Python way.
  • Click on the >> to see surrounding statements at that level.
  • Click on the + sign to get an interactive window where you can type in Python commands which will be executed in the context at that level.

Notice that you can use something like the following to break your application at any point and get a traceback:

raise RuntimeError, 'breaking at ...'

13.1.1   Using the Python debugger pdb

Try adding the following in your controller/action:

import pdb; pdb.set_trace()

Type "help" at the debugger prompt to see a list of commands, then type "help" followed by a command name for documentation on that command. Return to and continue on responding to the request by pressing "c".

Learn more about pdb, the Python debugger at The Python Debugger.

13.1.2   Using an IPython embedded shell

IPython provides a more powerfull interactive prompt and a powerful embedded shell. If you are a Python programmer and have not yet tried IPython, you definitely should look into it.

First, import from IPython -- Add something like the following at the top of your controller module, in our case in firstapp/controllers/firstcontroller.py:

from IPython.Shell import IPShellEmbed
args = ['-pdb', '-pi1', 'In <\\#>: ', '-pi2', '   .\\D.: ',
    '-po', 'Out<\\#>: ', '-nosep']
ipshell = IPShellEmbed(args,
    banner = 'Entering IPython.  Press Ctrl-D to exit.',
    exit_msg = 'Leaving Interpreter, back to Pylons.')

Then, place this code in your action/method:

ipshell('We are at action abc')

Return to Pylons and continue on responding to the request by pressing Ctrl-D.

Note that because of some idiosyncratic feature of IPython.Shell.IPShellEmbed, I had to put the following before each call to ipshell():

ipshell.IP.exit_now = False
ipshell('We are at action abc')

Learn more about IPython at IPython: an Enhanced Python Shell.

13.2   Production mode

When you are ready to go live, you will want to do (at least) two things:

  • Uncomment the following line in development.ini:

    set debug = false
    
  • Omit the --reload option when you use Python Paste to start your server. In other words, use:

    $ paster serve development.ini
    

    rather than:

    $ paster serve --reload development.ini
    

But, there are additional deployment options for Pylons applications. To learn about them, take a look under Testing, Upgrading, and Deploying -- http://pylonshq.com/docs/en/0.9.7/#testing-upgrading-and-deploying.