=============================== Pylons Quick Site Development =============================== :Author: Dave Kuhlman :address: dkuhlman@rexx.com http://www.rexx.com/~dkuhlman :revision: 1.0a :date: Feb. 12, 2007 :copyright: Copyright (c) 2006 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:: .. sectnum:: 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 `_. Installation ============ Follow the instructions at `Installing Pylons `_. Hopefully, you can install Pylons with the following:: $ easy_install Pylons 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: - `Genshi `_ - `Kid `_ - `Cheetah `_ 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 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 ` that is not in the standard Docutils distribution. 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. 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. 3. Start your new application. If you changed the port to 5004, for example, for example, then you should be able to view a default page in your Web browser at ``http://localhost:5004/``. 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('

firstapp default

') Now refresh the Web page ``http://localhost:5004/firstapp`` and you should see:: firstapp default And if it did not work: - You might need to restart your server. However, notice that if you started (served) your application with the ``--reload`` option as suggested above, you will not need to restart the server in order to see the new response. - You might need to delete the default Web page. When `Python Paste`_ generated your application, it also generated a default Web page. This Web page is static content generated in the ``firstapp/public`` directory. Pylons gives priority to static content over mapped (routed) content for a given URL, if you have both. You might have to remove that file, since we are using a URL that maps to it. 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:: Test #1

Test #1

<%python> items = ['one', 'two', 'three', 'four', ]
    % for item in items:
  1. Item <% item.capitalize() %>
  2. % # end for
The above template contains `Myghty`_ code. For the present, it may be enough to understand: - Python code inside ``<%python>`` and ```` is executed in the global namespace. We use that to set the global variable ``items``. - Control structures can be written in lines beginning with a "%" in column 1. In our case, we use this to generate one list item for each item in ``items``. - Expressions of the form ``<% expression %>`` are replaced with the value of the expression that they contain. See the `Myghty Documentation `_ for help with learning how to write this kind of content. 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``:: Test #2

Test #2

    % for item in c.random_values:
  1. <% item %>
  2. % # end for
Explanation: - Notice that, using the variable ``c``, we access ``c.random_values`` that we set in the controller. 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. 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``:: Test #4

Test #4

Hello. You want help, right?

**Caution:** Be aware that problems can occur when mixing wildcard parts with dynamic parts. See `Wildcard Limitations and Gotchas `_ for more on this. 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``:: Test #3

Test #3

Hello user <% c.userid %>.

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') 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. 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``:: Test #5

Test #5

Your current item number is <% c.itemnumber %> and your current color is <% c.color %>.

Item number: />

Color: />

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. 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``:: Test #6

Test #6

Your current item number is <% c.itemnumber %> and your current color is <% c.color %>.

<% h.form(h.url(action='test6'), method='put') %>
<% h.text_field('itemnumber', value=c.itemnumber) %>
<% h.text_field('color', value=c.color) %>
<% h.submit('Submit') %>
<% h.end_form() %> 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 `_. 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 `_. 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 ``dsn`` that we defined in ``development.ini``. 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'): 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() 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. 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. 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:: Test #7

Test #7 -- Plants Database

Your current item number is <% c.itemnumber %> and your current color is <% c.color %>.

% for plant in c.plants: % # end for
Name Description Rating
<% plant.p_name %> <% plant.p_desc %> <% plant.p_rating %>
<% h.form(h.url(action='test7'), method='put') %>
<% h.text_field('p_name', value=c.p_name) %>
<% h.text_field('p_desc', value=c.p_desc) %>
<% h.text_field('p_rating', value=c.p_rating) %>
<% h.submit('Add') %> <% h.submit('Delete') %> <% h.submit('Update') %> <% h.submit('Show') %>
<% h.end_form() %> 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: - routing goes in ``myapp/config/routing.py``. - Actions go in ``myapp/controllers/mycontroller.py`` where "mycontroller" is the argument you passed to ``paster``. For example:: $ paster controller mycontroller - Templates go under ``myapp/templates``. 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``. 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 `Using Sessions `_. 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``:: Test #8

Hello, user.

Count: <% c.count %>

A few additional notes on sessions: - The ``session`` object is available in your controller. You imported it with:: from firstapp.lib.base import * - Treat the session as a dictionary. For example, use:: 'mykey' in session or:: session.has_key('mykey') to check for the existence of a key. Use, for example:: if 'address' in session: address = session['address'] and:: session['address'] = address to get and set a value in the session. - Be sure to save the session before rendering your page:: session.save() A Few Additional Notes ====================== Debugging --------- When your 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 ...' 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 `_. 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 `_. 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`_ start your server. In other words, use:: $ paster serve development.ini rather than:: $ paster serve --reload development.ini .. _`Python Paste`: http://pythonpaste.org/ .. _`Mako`: http://www.makotemplates.org/ .. _`Myghty`: http://www.myghty.org/ .. _`Pylons Wiki`: http://pylonshq.com/project/ .. _`Pylons Web site`: http://pylonshq.com/ .. _`Routes`: http://routes.groovie.org/ .. _`Python Paste`: http://pythonpaste.org/ .. _`Web Helpers`: http://pylonshq.com/WebHelpers/index.html