From PHP to Python – my first application.

I’ve been coding in PHP for years now, and even though I would stray from time to time, it was always to fix a bug or add some small bit of functionality, and never to build a site in Ruby or Python “from the scratch”.

Recently, I moved all my domains onto one server and I wanted to create a basic holding page application. I decided this was a good opportunity to one-up my Python skills.

If you’ve only programmed websites in PHP, you’ll the world of Python slightly confusing. PHP is primarely a Web development language, regardless of the fact you can run it in command line. Python is a general-purpose, high level, programming language, that can be used for Web, as one of it’s many purposes.

There are many ways of running Python on your server, including CGI and FastCGI, but after some reading I decided to go with mod_python

To speed up web development, go with a framework. You don’t want to write all of the functions to communicate with the server etc. For the purposes of this exercise, I chose a small framework web.py

mod_python

mod_python is a brilliant module, that embeds the interpreter directly into the Apache process. This means the requests are faster, as the interpreter does not need to be started each time.

Run the command below to install mod_python

$ yum install mod_python

Like in my previous article about installing git, I had an issue with libraries being excluded from dependencies, so I had to edit

$ vim /etc/yum.conf

and remove the line httpd*. Run yum install again.

To load mod_python in the apache, find your LoadModule section in your httpd.conf and add the line below

LoadModule python_module modules/mod_python.so

Always test your apache config, in case it brings down your server, before you restart it.

$ http -t

If your config test It might be that yum installed your mod_python in a different directory, in which case run the commands below

$ updatedb&
$ locate mod_python.so
/usr/lib/httpd/modules/mod_python.so
$ cp /usr/lib/httpd/modules/mod_python.so /usr/local/apache/modules/

Run the httpd.conf test again, and if everything is ok, restart the Apache.

web.py

Web.py has been developed by Aaron Swartz to run reddit. It’s been described as antiframework, framework. I chose it, because it seemed simple enough as an introduction to Python. All of my examples are based on web.py’s tutorial but modified for my own needs, and include solutions to the problems I came across when implementing the framework.

For the tutorial below I have used Python 2.6, web.py 0.37

In order to start with web.py, we’ll have to install it. easy_install is a package manager for Python, if you don’t have it installed yet run the command below

$ yum install python-setuptools

Otherwise you’re good to go:

$ easy_install web.py

You will also need to set up our WSGI (Web Server Gateway Interface). According to WSGI wiki page:

The Web Server Gateway Interface (WSGI /ˈwɪzˌɡiː/) defines a simple and universal interface between web servers and web applications or frameworks for the Python programming language.

In order to set up mod_python as a WSGI we need to download modpython_gateway.py:

$ cd /usr/lib/python2.6/wsgiref
$ wget -O modpython_gateway.py http://svn.aminus.net/misc/modpython_gateway.py

Now let’s start coding

Holding Page app

My holding page will be pretty simple. I want it to display the name of the domain and a contact form. Let’s start with importing the modules I’m going to use.

import web
import os

Those two lines mean my script will use web.py and os.py libraries. First is the framework I’m using, the second, allows me to use OS specific functions, in my case, handles the directory path.

You can print html in your Python script, but web.py allows you to separate “business logic” from the view, and use templates. In the next step I’m going to specify my templates directory. First let’s create a directory that will hold all our templates in the directory our script will live in.

$ mkdir templates

An example on web.py tutorial suggests to use just:

render = web.template.render('/templates')

But that didn’t work for me. and instead I would get the error below:

raise AttributeError, "No template named " + name

There seems to be a problem with Python understanding where does “templates/” path refer to. Instead, I’m going to make use of the os module I imported above and get the full path name for the templates:

template_dir = os.path.abspath(os.path.dirname(__file__)) + "/templates"
render = web.template.render(template_dir)

The render will look for index.html in the templates directory, so let’s create it:

$ vim templates/index.html

<html><head><body>Hello world!</body></head></html>

Now let’s define the routes my application is going to support. I only really need one, index page.

urls = (
    '/', 'index',
)

The first part is a regular expression that matches the url, and the second is the class it’s going to replace it with.

Now let’s initiate it, by defining some of the key variables. One of them is the variable name which will hold the name of the site, and change for each instance when I re-use it.

app = web.application(urls, globals())  
main = app.wsgifunc()
name = "Site Name"

web.py makes a clear distinction between functionality in GET and POST request. My default action will be handled by the GET request, so I’m going to start my index class with defining it, and passing the previously defined variable name into the template.

class index:
        def GET(self):
           return render.index(name)

Users who end up on the site, should be able to contact me, so as we’re adding the name variable.

$def with (name)

<html>
    <head>
        <title>$name - Holding Page</title>
    </head>
    <body>
        Welcome to holding page for $name.
    </body>
</html>

Contact form

Users need to be able to contact me when they visit the landing page, so the next step will be to create a web form. web.py has it’s own module used for creating forms, user input, and validation. You can find the full manual for it in the form library documentation.

Let’s start by adding the form module to the libraries we import at the top of our script

from web import form

The only fields I’ll be needing are name, email, and comment, so I can initialise my form by defining the form.

Each of the form elements supports several attributes, with the first one being the name, and second potential validation filters, followed by other attributes in any order.

For my email field, I am creating a validation for the email field, and assigning it to the vemail variable

vemail = form.regexp(r".*@.*", "must be a valid email address")

comments_form = form.Form(
    form.Textbox('name', form.notnul),
    form.Textbox('email', vemail),
    form.Textarea('comment', form.notnul),
    form.Button('Send'),
)

Initiate it in the GET section

cform = comments_form();
name  = "Site Name"
return render.index(name, cform)

And in the template:

$def with (name, cform)
<html>
<head>
    <title>$name - Holding Page</title>
</head>
<body>
    Welcome to holding page for $name.

    <form action="/" method="POST">   
        $if not cform.valid: <p class="error">Try again:</p>
        $:cform.render()
    </form>
</body>
</html>

The only thing left to do is to create a new action for POST, and ask it to send out an email with the content of the form:

def POST(self):
    cform= comments_form()
    if cform.validates():
        from_email = cform.d.email
        subject    = "New email from %s" % (cform.d.name)
        message    = "Name: %s\n Email: %s\n Comment:\n%s\n" % (cform.d.name, cform.d.email, cform.d.comment)
        web.sendmail(from_email, "email@domain.com", subject, message)
    return render.index(name, cform)

Et voila. You should be able to post the form and get an email back.

Setting up Apache

Now that all of the functionality is done, let’s set up how the script is loaded in .htaccess. I want it to appear seamlessly when user enters http://www.site.com. For this, we need to set up .htaccess properly.

Let’s assign the .py extension to be recognized as a python-program:

AddHandler python-program .py

Setting up the WSGI for our application, and adding the path for the website to the Python path

PythonHandler wsgiref.modpython_gateway::handler
PythonOption wsgi.application holding_page::main

And lastly, let’s rewrite any url to the holidng_page.py script

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ holding_page.py/$1 [L]

If you got to http://sitename.com you should be able to see the page just fine.

I hope this helps you with your first Python application, and clears some issues you might occur while working with web.py. To make everything clear, I listed the scripts below, in full

Scripts in full.

holding_page.py

import web
import os

from web import form

template_dir = os.path.abspath(os.path.dirname(__file__)) + "/templates"
render = web.template.render(template_dir)

urls = (
  '/', 'index',
)

vemail = form.regexp(r".*@.*", "must be a valid email address")

comments_form = form.Form(
    form.Textbox('name', form.notnull),
    form.Textbox('email', vemail),
    form.Textarea('comment', form.notnull),
    form.Button('Send'),
)

app  = web.application(urls, globals())
main = app.wsgifunc()
name = "Site name"

class index:
    def GET(self):
        cform = comments_form()
        return render.index(name, cform)

    def POST(self):
        cform= comments_form()
        if cform.validates():
            from_email = cform.d.email
            subject    = "New email from %s" % (cform.d.name)
            message    = "Name: %s\n Email: %s\n Comment:\n%s\n" % (cform.d.name, cform.d.email, cform.d.comment)
            web.sendmail(from_email, "kasia@gogolek.co.uk", subject, message)
        return render.index(name, cform)

if __name__ == "__main__": app.run()

templates/index.html

$def with (name, cform)
<html>
<head>
    <title>$name - Holding Page</title>
</head>
<body>
    Welcome to holding page for $name.
        <form action="/" method="POST">
            $if not cform.valid: <p class="error">Try again</p>
            $:cform.render()
        </form>
</body>
</html>

.htaccess

AddHandler python-program .py
PythonHandler wsgiref.modpython_gateway::handler
PythonOption wsgi.application holding_page::main

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ holding_page.py/$1 [L]