PylonsHQ.

Layout: Fixed-width

Making a Pylons Blog

Skip to end of metadata
Go to start of metadata

Updated for Pylons 1.0 and SQLAlchemy 0.5.

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.

TODO:

Step 1 - Building the Backdrop

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.

1
2
3
4
5
6
easy_install Pylons 
easy_install SQLAlchemy

easy_install pysqlite             # for sqlite (only needed for Python 2.4 or older)
easy_install MySQL-python         # for MySQL
easy_install psycopg2             # for PostgreSQL

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.

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.

1
2
3
4
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

Step 1.3 - Models and Data

You can specify the database to use in development.ini

~/MyBlog/development.ini

Put this in the "[app:main]" section.

For SQLite (this is, in fact, the default setting created by paster create):

1
sqlalchemy.url = sqlite:///%(here)s/development.db

For Postgres:

1
sqlalchemy.url = postgres://username:password@host:port/database

For MySQL:

1
sqlalchemy.url = mysql://username:password@host:port/database

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

~/MyBlog/myblog/model/__ init __.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import sqlalchemy as sa
from sqlalchemy import types

from myblog.model.meta import Session, Base


def init_model(engine):
    """Call me before using any of the tables or classes in the model"""
    Session.configure(bind=engine)


class Blog(Base):
    __tablename__ = "Blog"
    __mapper_args__ = dict(order_by="date desc")

    id = sa.Column(types.Integer, primary_key=True)
    subject = sa.Column(types.Unicode(255))
    author = sa.Column(types.Unicode(255))
    date = sa.Column(types.DateTime())
    content = sa.Column(types.Text())

The attributes of Blog tell 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.

The order_by argument means all queries will return Blog objects in descending date order; i.e., most recent first.

Blog is our object-oriented class we'll use to access the table. We could also access the blog table directly at the SQL level, but that is not the subject of this tutorial.

Base is a base class for ORM classes. It has a reference to a MetaData 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.

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.

~/MyBlog/myblog/websetup.py
In Pylons 1.0, 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.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
"""Setup the MyBlog application"""
import logging

import pylons.test

from myblog.config.environment import load_environment
from myblog.model.meta import Session, Base

log = logging.getLogger(__name__)

def setup_app(command, conf, vars):
    """Place any commands to setup myblog here"""
    # Don't reload the app if it was loaded under the testing environment
    if not pylons.test.pylonsapp:
        load_environment(conf.global_conf, conf.local_conf)

    # Create the tables if they don't already exist
    log.info("Creating database tables")
    Base.metadata.create_all(bind=Session.bind)
    log.info("Finished setting up")

Now run the following the command in ~/MyBlog:

1
paster setup-app development.ini

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

If you get an error saying the database doesn't exist, you'll have to create it in a database-specific manner. PostgreSQL uses "createdb DATABASE_NAME". 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.

Step 2 - Putting the script together

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:

1
paster controller blog

Now we need to give the actors their lines.

~/MyBlog/myblog/controllers/blog.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import logging

from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect

from myblog.lib.base import BaseController, render
from myblog import model

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.mako")

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.

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.)

1
mkdir ~/MyBlog/myblog/templates/blog #  this creates the blog directory

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

~/MyBlog/myblog/templates/blog/index.mako

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<%inherit file="site.mako" />
<%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

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.

~/MyBlog/myblog/templates/blog/site.mako

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<%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>

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

The syntax and template inheritance is explained in the excellent Mako manual.
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.

This example escapes any HTML markup the user enters in their blog post, since Pylons configures Mako to escape all strings by default. If you want to allow your users to type raw HTML – which is a security hole, if the user isn't trusted – use ${post.content|n}.

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.

~/MyBlog/myblog/config/middleware.py

At the top of the file put:

1
from paste.translogger import TransLogger

Below "# CUSTOM MIDDLEWARE HERE" put:

1
2
3
4
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")

This will only log requests handled by the MyBlog application. It will ignore
static file requests. If you want those to be included as well, move the
TransLogger to the very end, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# ...
    if asbool(static_files):
        # Serve static files
        static_app = StaticURLParser(config['pylons.paths']['static_files'])
        app = Cascade([static_app, app])

    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")

    app.config = config
    return app

Step 3 - The first rehearsal: Testing the application

Run the application using (inside ~/MyBlog):

 paster serve --reload development.ini

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.

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/index" outputs. Add a routing rule going to that action.

~/MyBlog/myblog/config/routing.py

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

1
map.connect('/', controller='blog', action='index')

You'll see there's already a couple of routes underneath it: "map.connect('{controller}/{action}')' and "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.

Step 4 - Adding to the cast!

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:

1
paster controller toolkit

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..

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".

1
mkdir ~/MyBlog/myblog/templates/toolkit

~/MyBlog/myblog/templates/toolkit/index.mako

1
2
3
4
5
6
7
8
9
<%inherit file="/blog/site.mako" />
<%def name="title()">Admin Control Panel</%def>

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

url 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.)

~/MyBlog/myblog/templates/toolkit/add.mako

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<%inherit file="/blog/site.mako" />
<%def name="title()">Add Blog Post</%def>

<span class="h3"> Post a Comment </span>
${h.form(url(controller='toolkit', action='blog_add_process'))}
<label>Subject: ${h.text('subject')}</label><br>
<label>Author: ${h.text('author')}</label><br>
<label>Post Content: ${h.textarea('content')}</label><br>
${h.submit('post', 'Post New Page')}
${h.end_form()}

~/MyBlog/myblog/lib/helpers.py

Add

1
from webhelpers.html.tags import form, text, textarea, submit, end_form

~/MyBlog/myblog/controllers/toolkit.py

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import datetime
import logging

from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect

from myblog.lib.base import BaseController, render
from myblog import model

log = logging.getLogger(__name__)

class ToolkitController(BaseController):

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

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

    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 a value automatically.
        
        # Attach the object to the session.
        model.Session.add(newpost)

        # Commit the transaction.
        model.Session.commit()

        # Redirect to the blog home page.
        redirect(url(controller="blog", action="index"))

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.

Labels

sqlalchemy sqlalchemy Delete
model model Delete
database database Delete
recipes recipes Delete
tutorial tutorial Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

Powered by Pylons - Contact Administrators