View Source

Updated for Pylons 0.9.6 and SQLAlchemy 0.5. (Currently being updated for Pylons 0.9.7.)

For people who haven't used Pylons before, think about it like putting on a theatrical production. First of all you need a stage or backdrop, then we put together a script so we can show off the cast. Pylons, as a framework, runs in the background like the backdrop and stage hands. The play you're producing is called an "application" or "project" in Pylons terminology, so we'll use those terms too.

A Python egg corresponding to some version of this article is under the "Attachments" tab.

If you have any SQLAlchemy questions not addressed here, see the excellent [SQLAlchemy manual|http://www.sqlalchemy.org/docs/04/index.html].

{note}
*TODO:*
* transaction log
* authentication
* modifying and deleting posts (see comments at bottom of http://wiki.pylonshq.com/display/~gardsted/Making+a+Pylons+Blog+revisited)
* RESTful URLs (map.resource in routing.py)
* reader comments (this is probably too big an endeavor for a tutorial)
{note}

h1. Step 1 - Building the Backdrop

h2. Step 1.1 - Getting the Odds and Ends

Install Pylons, SQLAlchemy, the database engine you intend to use, and the Python interface to that database engine.
There are several other documents around describing how to install these, so we'll just list the Python packages you'll need.

{code:bash}
easy_install Pylons
easy_install SQLAlchemy

easy_install pysqlite # for sqlite (included in Python 2.5)
easy_install MySQL-python # for MySQL
easy_install psycopg2 # for PostgreSQL
{code}

SQLite is an easy-to-install engine for learning with, and an adequate choice for non-huge projects. MySQL and PostgreSQL are good production workhorses. SQLAlchemy also supports Oracle, MS-SQL, Firebird, ODBC, and other engines.

h2. Step 1.2 - Erecting the Pylons

cd to your projects directory. Do _not_ use your webserver's "htdocs" directory, as this will publish your application's source files rather than the running application, which would be a security hole if the source contains passwords. If you have no other place, use your home directory. The example will use "/home/odin", abbreviated "~". The project will be called MyBlog.

{code:bash}
cd /home/odin # I am Odin, the chief Norse god.
paster create -t pylons MyBlog # This will show a whole lot of output.
# When paster asks you to include SQLAlchemy, type True.
cd MyBlog # You'll now be in /home/odin/MyBlog
{code}

h2. Step 1.3 - Models and Data

We'll set up the database according to the [Using SQLAlchemy with Pylons|pylonsdocs:Using SQLAlchemy with Pylons] tutorial. (We'll just add an {{init_model}} function that will soon be added to that tutorial.)

{panel:title=~/MyBlog/development.ini| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}
Put this in the "\[app:main]" section.

For MySQL:
{code:ini}
sqlalchemy.url = mysql://username:password@host:port/database
sqlalchemy.pool_recycle = 3600
sqlalchemy.convert_unicode = true
{code}

For SQLite:
{code:ini}
sqlalchemy.url = sqlite:///%(here)s/db.sqlite
sqlalchemy.convert_unicode = true
{code}

For Postgres:
{code:ini}
sqlalchemy.url = postgres://username:password@host:port/database
sqlalchemy.convert_unicode = true
{code}

The SQLite example puts the database in the same directory as development.ini.

Because databases cannot store Unicode directly, the "sqlalchemy.convert_unicode" option makes SQLAlchemy convert String columns to UTF-8 on write, and back to Unicode on read. Otherwise you'd get "str" strings containing whatever's in the database verbatim. (This is preferred over MySQL's "?use_unicode=1" option because it's database neutral.)
{panel}

{panel:title=~/MyBlog/myblog/config/environment.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

{note}
If you are using Pylons 0.9.7, you should skip this step as it is already done for you.
{note}

Add this at the top of the module:

{code:python}
import sqlalchemy as sa
from myblog import model
{code}

And put this at the end of the {{load_environment}} function:

{code:python}
engine = sa.engine_from_config(config, "sqlalchemy.") # notice there is trailing dot
model.init_model(engine)
{code}

{{load_environment}} is called when your application starts running. Here we create a SQLAlchemy "engine", which is a pool of connections to a certain database, and bind it to the model (which we haven't created yet).
{panel}

{panel:title=~/MyBlog/myblog/model/__ init __.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}
{code:python}
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy import types

def init_model(bind):
"""Call me at the beginning of the application.
'bind' is a SQLAlchemy engine or connection, as returned by
sa.create_engine, sa.engine_from_config, or engine.connect().
"""
global engine, Session
engine = bind
Session = orm.scoped_session(
orm.sessionmaker(transactional=True, autoflush=True, bind=bind))
orm.mapper(Blog, blog_table,
order_by=[blog_table.c.date.desc()])

meta = sa.MetaData()

blog_table = sa.Table("Blog", meta,
sa.Column("id", types.Integer, primary_key=True, autoincrement=True),
sa.Column("subject", types.String(255)),
sa.Column("author", types.String(255)),
sa.Column("date", types.DateTime()),
sa.Column("content", types.Text()), #or types.TEXT() - to .Text() doesn't exists :/
)

class Blog(object):
def __str(self):
return self.title
{code}

Why is some code in {{init_app}} and other code isn't? The code in {{init_app}} depends on a live database connection, which is not available when the model is imported. All programs that use the model will have to initialize a SQLAlchemy engine on their own, and call {{init_model(engine)}} before using any of the tables or classes in the model. "bind" is a SQLAlchemy argument convention meaning either an engine or a connection is acceptable. It also gets around the fact that we can't set a global variable with the same name as an argument.

{{blog_table}} tells SQLAlchemy the table structure (called a schema), so that it can create and use the table. It's possible to autoload the schema from an existing database table rather then specifying the columns ourselves, but that depends on a live connection so it would have to be done in {{init_app}}. {{blog_table.c}} contains the table columns as attributes.

{{Blog}} is our object-oriented class we'll use to access the table. We could also access {{blog_table}} directly at the SQL level, but that is not the subject of this tutorial. The {{orm.mapper}} call links the table to our {{Blog}} class.
The {{order_by}} argument means all queries will return {{Blog}} objects in descending date order; i.e., most recent first.

{{meta}} is an object SQLAlchemy uses to hold information about tables. If you had multiple databases with overlapping table names, you'd need one {{MetaData}} object for each database.

{{Session}} is a SQLAlchemy session manager, used to coordinate access to mapped classes like {{Blog}}. It is _not_ related to {{pylons.session}}, which is an HTTP session. {{Session}} is called a "scoped session" in the SQLAlchemy manual. That means it's not a session object itself but a thread-safe wrapper around it. Don't worry about the difference. {{Session}}'s class methods call the same-named methods in hidden session objects; that's all you need to know.

{{autoflush=True}} means SQLAlchemy will automatically write changes to the database so we don't have to call {{Session.save()}}. {{transactional=True}} puts all database access in a SQL transaction, so we'll call {{Session.commit()}} to save our changes permanently or {{Session.rollback()}} to discard them. These flags are usually both true or both false in applications. MySQL MyISAM tables are non-transactional so you can't actually roll back changes on them; you'll get an error if you call {{.rollback}} in this case.

{{bind=engine}} tells the session to use this database for all database access. If we had some tables in one database and other tables in another database, we'd use the {{binds}} argument instead.
{panel}


{panel:title=~/MyBlog/myblog/websetup.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

{note}
In Pylons 0.9.7, this is mostly done for you. However, you may want to add the {{log.info}} calls for more informative console output during {{paster setup-app}}.
{note}

Now edit ~/MyBlog/myblog/websetup.py so it looks like this:

{code:python}
"""Setup the MyBlog application"""
import logging

from paste.deploy import appconfig
from pylons import config

from myblog.config.environment import load_environment
from myblog import model

log = logging.getLogger(__name__)

def setup_config(command, filename, section, vars):
"""Place any commands to setup myblog here"""
conf = appconfig('config:' + filename)
load_environment(conf.global_conf, conf.local_conf)
log.info("Creating database tables")
model.meta.create_all(bind=model.engine)
log.info("Finished setting up")
{code}
{panel}

{panel:title=edit ~/MyBlog/myblog/lib/base.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

{note}
If you are using Pylons 0.9.7, you should skip this step as it is already done for you.
{note}

Replace the {{WSGIController.\_\_call\_\_}} line with this:

{code:python}
# (ignore this line; it's to fix the indentation)
def __call__(self, environ, start_response):
try:
return WSGIController.__call__(self, environ, start_response)
finally:
model.Session.remove()
{code}

This prevents session data from "leaking" from one Web request to the next.
{panel}


Now run the following the command in ~/MyBlog:

{code:bash}
paster setup-app development.ini
{code}

This will configure the above blog database and we can move on to templates and some actual Python!

{note}
If you get an error saying the database doesn't exist, you'll have to create it in a database-specific manner. MySQL uses the command "mysqladmin create DATABASE_NAME". You may also have to set access permissions using the "GRANT" statement in the {{mysql}} command-line tool. This is another reason to use SQLite when just starting out, because SQLite automatically creates the database file without any fuss.
{note}

h1. Step 2 - Putting the script together

h2. Step 2.1 - The First Part
In Pylons, the application is based on a series of "controllers." They are like the actors of a play, each playing their part.
So let's make a blog controller!
Again in ~/MyBlog:

{code:bash}
paster controller blog
{code}

Now we need to give the actors their lines.

{panel:title=~/MyBlog/myblog/controllers/blog.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

{note}
In Pylons 0.9.7, {{model.Session}} may not exist. Instead, you should import {{Session}} from {{myblog.meta}} and use that instead.
{note}

{code:python}
import logging

from myblog.lib.base import *

log = logging.getLogger(__name__)

class BlogController(BaseController):

def index(self):
q = model.Session.query(model.Blog)
c.posts = q.limit(5)
return render("/blog/index.html")
{code}

This retrieves the last 5 blog posts in the database and passes them to the template via Pylons' {{c}} global. Actually it passes the unexecuted database query, which gives the template a bit more flexibility to ask different questions of the result set. {{q}} is a query referring to all records in the table. {{c.posts}} is a query referring to the first five records. To execute the query and get the results as a list of {{Blog}} objects, append {{.all()}} to it.
{panel}

h2. Step 2.2 - Making the template

We want to store our blog-related templates inside their own subdirectory called 'blog'. (This is not necessary; we could put them directly in the 'templates' directory.)

{code:bash}
mkdir ~/MyBlog/myblog/templates/blog # this creates the blog directory
{code}

Now create a new text file called index.html inside ~/MyBlog/myblog/templates/blog.

{panel:title=~/MyBlog/myblog/templates/blog/index.html| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

{code:html+mako}
<%inherit file="site.html" />
<%def name="title()">MyBlog Home</%def>

<p>${c.posts.count()} new blog posts!</p>

% for post in c.posts:
<p class="content" style="border-style:solid;border-width:1px">
<span class="h3"> ${post.subject} </span>
<span class="h4">Posted on: ${post.date} by ${post.author}</span>
<br>
${post.content}
</p>
% endfor
{code}

The {{for}} statement runs through all the posts found by the query and prints them out. The {{.count()}} call is just a silly example of asking another question on the same query.
{panel}

{panel:title=~/MyBlog/myblog/templates/blog/site.html| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

Now create a site template with boilerplate HTML for all your pages.

{code:html+mako}
<%def name="title()"></%def>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>MyBlog: ${self.title()}</title>
</head>
<body>
<h1>${self.title()}</h1>

<!-- *** BEGIN page content *** -->
${self.body()}
<!-- *** END page content *** -->

</body>
</html>
{code}

(The author refuses to use XHTML, believing it's a dead end and HTML 5 will supercede it.)

The syntax and template inheritance is explained in the excellent [Mako manual|http://www.makotemplates.org/docs/].
The only tricky thing here is the "title" function, used to make the page's title accessible to the site template. Hopefully Mako will evolve a better way to do this.

{note}
This example displays any HTML markup the user enters in their blog post, which is a security hole if the user is untrusted. See Mako's "h" filter for a solution to this.
{note}
{panel}

h2. Step 2.3 - Transaction log

This step is optional but is useful for debugging. It prints an Apache-style log line whenever somebody makes a Web request to the application.

{panel:title=~/MyBlog/myblog/config/middleware.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

At the top of the file put:

{code:python}
from paste.translogger import TransLogger
{code}

Below "# CUSTOM MIDDLEWARE HERE" put:

{code:python}
format = ('%(REMOTE_ADDR)s - %(REMOTE_USER)s [%(time)s] '
'"%(REQUEST_METHOD)s %(REQUEST_URI)s %(HTTP_VERSION)s" '
'%(status)s %(bytes)s')
app = TransLogger(app, format=format, logger_name="access")
{code}

{panel}

h2. Step 3 - The first rehearsal: Testing the application

Run the application using (inside ~/MyBlog):
{noformat} paster serve --reload development.ini{noformat}

Now navigate to: http://localhost:5000/blog

This will print out the latest five blog posts in the database (in order!). Of course there aren't any posts yet, so it simply displays "0 blog posts".

Because we used "\-\-reload", the HTTP server will restart whenever we modify a file. When you get bored, type your system's stop character (ctrl-C on Unix, maybe ctrl-Z on Windows or command-. on Macintosh) to quit the server. You may have to stop and restart the server manually if paster doesn't realize you've added a module.

h2. Step 3.1 - Home page

If you navigate to http://localhost:5000/, you'll get a default home page. To fix this, delete the file ~/MyBlog/myblog/public/index.html. Now you'll get a Not Found error, which is not much better. What you'd like instead is the list of new blog posts, the same as "/blog" outputs. Add a routing rule going to that action.

{panel:title=~/MyBlog/myblog/config/routing.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

Under "# CUSTOM ROUTES HERE", add this line:

{code:python}
map.connect('', controller='blog', action='index')
{code}

You'll see there's already a route underneath it: "map.connect(':controller/:action/:id')'. This is what's making all our other URLs work. It says that any URL that looks like "/CONTROLLER_NAME/ACTION_NAME" should be routed to that controller and action. (For security it won't call any action that begins with an underscore (\_), but instead return a "Not Found" error.) The "id" field is not used in our examples; you could use it with a URL like "/blog/show/1" for instance.

{panel}

h1. Step 4 - Adding to the cast!

h2. Step 4.1 - Acting Parts for Stagehands (The admin frontend)

Now, choose a suitable name for your admin toolkit to live in. In this tutorial I'll use "toolkit". from inside ~/MyBlog do:

{code:python}
paster controller toolkit
{code}

Now you have a blank controller that will just give you a "hello world" on http://127.0.0.1:5000/toolkit/

Why do we want a separate controller and not just add it to the blog controller? It's easier to force different permission levels onto a whole controller, uses less code, etc..

h2. Step 4.2 - Adding Content

Now we have an empty toolkit, what do we want in it? I've been told some people actually use a Web interface to do things, so we'll start by making an "Add a Blog Post" page. To make it simple for now, we'll leave the pretty templates and centralised linking for later.

We want to store our toolkit-related templates inside their own directory called 'toolkit'
We start with to simple templates called "index.mako" and an "add.mako".

{code:bash}
mkdir ~/MyBlog/myblog/templates/toolkit
{code}


{panel:title=~/MyBlog/myblog/templates/toolkit/index.html| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}
{code:html+mako}
<%inherit file="/blog/site.html" />
<%def name="title()">Admin Control Panel</%def>

This is home of the toolkit. <br>
For now you can only
<a href="${h.url_for(controller="toolkit", action="blog_add")}">add</a>
blog posts.
<p>
Later on you'll be able to delete and edit also.
{code}

{{h.url_for}} is the preferred way to make hyperlinks in templates because it adjusts to various deployment scenarios. (E.g., embedding an application in another, so the inner app has a prefix on all URL paths.)


{panel}

{panel:title=~/MyBlog/myblog/templates/toolkit/add.html| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}
{code:html+mako}
<%inherit file="/blog/site.html" />
<%def name="title()">Add Blog Post</%def>

<span class="h3"> Post a Comment </span>
${h.start_form('/toolkit/blog_add_process')}
<label>Subject: ${h.text_field('subject')}</label><br>
<label>Author: ${h.text_field('author')}</label><br>
<label>Post Content: ${h.text_area('content')}</label><br>
${h.submit('Post New Page')}
${h.end_form}
{code}

{panel}


{panel:title=~/MyBlog/myblog/controllers/toolkit.py| borderStyle=dashed| borderColor=#ccc| titleBGColor=#CCCCFF| bgColor=#FFFFFF}

{note}
For Pylons 0.9.7, see the above note about {{model.Session}}.
{note}

Lets make the changes to toolkit.py in the myblog/myblog/controllers directory:

{code:python}
import datetime
import logging

from myblog.lib.base import *

log = logging.getLogger(__name__)

class ToolkitController(BaseController):

def index(self):
return render('/toolkit/index.html')

def blog_add(self):
return render('/toolkit/add.html')

def blog_add_process(self):
# Create a new Blog object and populate it.
newpost = model.Blog()
newpost.date = datetime.datetime.now()
newpost.content = request.params['content']
newpost.author = request.params['author']
newpost.subject = request.params['subject']
# I didn't set ID because it will get an autoincrement value.

# Attach the object to the session.
model.Session.add(newpost)

# Commit the transaction.
# (This sends the SQL INSERT command due to autoflushing.)
model.Session.commit()

# Redirect to the blog home page.
#redirect_to(controller="blog", action="index") # This isn't working for me.
redirect_to("/blog")
{code}
{panel}

Check out http://127.0.0.1:5000/toolkit/blog_add and see your new form! You should now be able to post data, and visit the main blog page ( http://127.0.0.1:5000/blog ). This will output what you wrote.