cookbook/cookbook

Rolling with web2py

(formerly known as Gluon) created by Massimo Di Pierro

Perhaps you have heard of web2py, the new kid on the block of Web Frameworks. web2py is written in Python so it is more solid and much faster than Ruby on Rails. web2py is also a web application itself so you can do all development, deployment and maintenance of your applications through your web browser and that makes it easier to use than any other framework. Moreover web2py ships in one complete package (for Windows, Mac or Unix/Linux) including everything you need to start development (including Python, SQLite3, and multi-threaded web server).

You can get web2py here: http://www.web2py.com This document is intentionally designed to mimic http://onlamp.com/pub/a/onlamp/2005/01/20/rails.html so that you can compare web2py with Rails.

What is Python?

Python is an object oriented programming language designed to be super easy to teach without any compromise on functionality. Most Java algorithms can be rewritten in Python in one tenth of their original length. Python comes with an extensive set of portable standard libraries including support for many standard internet protocols (http, xml, smtp, pop, and imap, just to mention a few) and APIs to the Operating System.

What is web2py?

web2py is an open source web framework written in Python and programmable in Python for fast development of database-driven web applications. There are many web frameworks today including Ruby on Rails, Django, Pylons and Turbo Gears, so why another one? I developed web2py with the following goals in mind:

  1. As similar as possible to Rails but in Python, so that it is more solid and much faster.

  2. All-in-one package with no installation, no configuration and no shell scripting required.

  3. Be super easy to teach (my job is to teach). So I made web2py itself as a web application.

  4. Top-down design so that the web2py APIs would be stable from day one.

Seeing is Believing

Programming web2py is as easy as programming Rails but, if you do not know Python nor Ruby, web2py is easier to learn than Rails.

What is most important is that web2py requires much less code than J2EE equivalent or PHP equivalent, while enforcing a vary good and safe programming style.

web2py prevents directory traversal, SQL injections, cross site scripting, and reply attack vulnerabilities.

web2py manages session, cookies and application errors for you. All application errors result in a ticket issued to the user and a log entry for the administrator.

web2py writes all the SQL for you. It even creates the tables and decides when to do a migration of the database.

Give it a try.

Installing the Software

Go to http://mdp.cti.depaul.edu/examples and download the Windows, Mac or Unix files.

If you choose to use the Windows or Mac version you do not need anything else: unzip the file and click on web2py.exe or web2py.app respectively.

If you choose to use the Unix version you need the Python interpreter (version 2.4 or later) and the SQLite3 database. After you have those, unzip web2py and run

python web2py.py

In a production setting you should use PostgreSQL or MySQL and not SQLite3. From the web2py prospective that is as easy as changing one line in the program but that is not discussed here since you do not need it for development.

Running web2py

At startup web2py asks one question: “choose the administrator password” Choose one. After that web2py will open a web browser for you (remember no commands to type ever!) showing this welcome page

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image1.png

Click on “administrative interface”

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image2.png

and type the password that you choose at startup. You will be redirected to the “site” page of the administrative interface:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image3.png

Here you can:

  • install and uninstall applications

  • create and design (edit) your applications

  • cleanup error logs and sessions

  • byte-code compile applications for distribution and faster execution

web2py comes with three applications: admin (the administrative interface itself), examples (interactive documentation), and welcome (a basic template for any other application).

Let’s Write Code

We’ll create an online collaborative cookbook for holding and sharing recipes. We want our cookbook to:

  • Display a list of all recipes.

  • Create new recipes and edit existing recipes.

  • Assign a recipe to a category (like “dessert” or “soup”).

If you like, you can download the complete web2py Cookbook example and follow along.

Creating an Empty web2py Application

To start a new application type a name in the appropriate field, in our case cookbook, and press the button submit:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image4.png

A new web2py application is not empty but it is a clone of the welcome applicaiton. It contains a single controller, a single view, a base layout, a generic view and its own database administrative interface called appadmin (not to be confused with admin, the site-wide administrative interface).

Testing the Empty Web Application

You are already running web2py web server so there is nothing to test really. Anyway, click on cookbook/design and you will see

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image5.png

Here is where you can view/create/edit the components of your application. Under Controllers there is a file called default.py which “exposes index” If you click on index your newly created application will “welcome you”

web2py Model View Controller Design

Any web2py application is comprised of:

  • Models: files that contain a description of the data stored by your application. For example the fields in the tables of your databases, their relations, and requirements. web2py tells you which tables are defined in each model file.

  • Controllers: files that contain the logic of your application. Each URL is uniquely mapped into a function in a controller file. That function can generate a page, delegate a view to render a page, redirect to another URL or raise an exception (depending on the exception that may result in a ticket being issued or in a HTTP error page). web2py tells you which functions are exposed by each controller file.

  • Views: files that contain HTML and special {{ }} tags which render in HTML variables returned by the controller. This is the presentation layer of your application. web2py tells you when a view extends or imports other views.

  • Languages: files that contain translation tables for all strings (those that you explicitly mark as language dependent) for any of the languages you want to support.

  • Static files: all other files, including images, CSS, JavaScript, etc.

Notice that you do not need an editor nor you need to know the web2py directory structure since you can create and edit files from the design page.

Also notice that while it is good policy to give a view to every controller function (called action in rails), you do not have to since web2py always provides a generic.html view that will render any page that is missing a template.

URLs and Controllers

This image represents the general structure of web2py’s core functiy

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image6.png

A URL like http://hostname/cookbook/default/index/bla/bla/bla?variable=value will result in a call to function index() in controller default.py in applicaiton cookbook.

“bla”, “bla”, and “bla” will be passed as request.args[0:3] while “value” will be stored in request.vars.variable.

Controller functions should return a dictionary like in

return dict(name=value, othername=othervalue)

and the variables name and othername will be passed to the associated view.

Try now, from cookbook/design, to create a test.py controller (just type the name and click submit), edit test.py and create your own index function

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image7.png

go back to cookbook/design and click on the index function exposed by test.py.

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image8.png

web2py is using the generic.html view, which extends the basic layout.html, to render the variable text returned by your index() function.

The excitement Begins...

Creating the Model

Go to cookbook/design and create a new model called db.py (just type db in the apposite field and click submit). The definition of a model here is slightly different than in Rails. In web2py a model is a single file that contains a definition of all tables in each database.

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image9.png

Edit the just created db.py model and write the following:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image10.png

This model defined two tables category and recipe. recipe has a field category that is a reference to db.category and field date that default to today. Each field has some requirements (this is optional), category.name requires that a new value IS_NOT_IN_DB (the field must be unique), recipe.category requires that the field IS_IN_DB (the reference is valid), recipe.date requires that it contains a valid date.

These requirements will be enforced in any entry form, whether part of the administrative interface or user generated.

The Database Administrative Interface (appadmin)

Go to cookbook/design and, under model, you will see two new links database administration and sql.log. Click on the former and if you do not have typos you will see:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image11.png

This is your application administrative interface. Try to insert a new category record:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image12.png

and some new recipes:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image13.png

Wasn’t this easier than Rails? Let’s not even compare with PHP, JSP, ASP, J2EE, etc.

Who created the tables? web2py did! web2py looked for a database called db.db, could not find one so it created the database and the tables you just defined. If you modify a table definition, web2py will alter the table for you (SQLite3 only supports adding fields, Postgresql also supports dropping fields). If you define another table it will be created. You can look at the SQL generated by web2py for this migration by clicking on sql.log.

Feel free to explore the administrative interface, insert a few records and try to list them.

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image14.png

The table is sortable by clicking on the header and will paginate if you have more than 100 items. Try a JOIN by typing “recipe.category=category.id” in the SQL FILTER field.

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image15.png

Where did field id come form? In web2py every table has a unique integer key called id. If you click on the id value in the table you will be able to edit the individual record.

Notice that appadmin.py is part of your cookbook application so you can read it and modify it. In this tutorial we choose not to do it and we prefer to take the longer route and write a new controller from scratch. We believe this better serves our didactic purpose.

Creating Functions (Actions)

While in cookbook/design, edit the test.py controller and add the following:

def recipes():
    records=db().select(db.recipe.ALL,orderby=db.recipe.title)
    return dict(records=SQLTABLE(records))

Now back in design, click on “recipes” and you should see

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image16.png

Notice that the variable records passed to the 11049004876800view is a SQLTABLE that knows how to render itself in CSS friendly HTML. The variable records is rendered by the generic.html view.

Let’s customize this more. Change the controller into:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image17.png

Notice how:

  • recipes now returns a list of records, not an SQLTABLE, moreover it generates a selection form from the category field of the table.

  • show takes the request.vars.id and performs select, on failure it redirects to recipes

  • new_recipe returns a SQLFORM object which builds an HTML form from the definition of a table (db.recipe). form.accepts() performs validation of the form (according to the requirements in the model), updates the form with error messages and, on successful validation, it inserts the new record in the database.

  • URL(r=request,f='function' generates the url for “function” in the current application and controller as determined by the HTTP request.

This code is already fully working using the generic view but we will perform additional customization at the layout layer below.

Notice that some validators, like IS_DATETIME() for a ‘datetime’ field, are automatically set by default.

Creating Views

Now create a view for recipes. This view is called test/recipes.html (type the name with path in the opposite field and click submit).

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image18.png

Edit the newly created file

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image19.png

Now try the calling recipes again

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image20.png

Notice that the code inside {{ }} tags is Python code with some caveats:

  • There is no indentation requirement, a block of code starts with a line ending in colon and ends with a line starting with pass (exemptions are def:return, if:elif:else:pass and try:except:pass).

  • The view sees everything defined in the model plus the variables returned by the controller.

  • {{=something}} will render something in HTML after escaping special characters.

Notice that

{{=A(message,_href=link)}}

is an HTML helper. It simply writes the

<a href= "link">message</a>

tag for you.

Create a view test/show.html that contains:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image21.png

It will look like this:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image22.png

Finally create a test/new_recipe.html that contains:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image23.png

It will look like this:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image24.png

Notice how web2py capitalized the names of the fields in the form and generated a SELECT/OPTION for the category field based on the specified requirements.

If you do no like the [web2py]cookbook banner or the CSS you can edit them both in the layout.html file.

Some Magic

If you try to submit a form that does not meet the requirements (for example try to submit an empty recipe), web2py will notify you about that.

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image25.png

Conclusions

We have written a working web2py application with only the browser, a few clicks and a total of 53 lines of code. We also got for free a database administrative interface that allows to insert, select, update and delete individual records or record sets.

web2py also includes easy to use functions to import/export tables in CSV, to generate RSS feeds and RTF files (compatible with MS Word), and to handle JSON for AJAX.

To read more about web2py visit the web page: http://mdp.cti.depaul.edu

If you have questions, please join our Google group: http://groups.google.com/group/web2py?hl=en

Appendix. The Database API

Connect to a sqlite3 database in file test.db

>>> db=SQLDB("sqlite://test.db")

or connect to a MySQL database

>>> db=SQLDB("mysql://username:password@host:port/dbname")

or connect to a PostgreSQL database

>>> db=SQLDB("postgres://username:password@host:port/dbname")

Available field types

>>> tmp=db.define_table('users',\
    SQLField('stringf','string',length=32,required=True),\
    SQLField('booleanf','boolean',default=False),\
    SQLField('passwordf','password'),\
    SQLField('textf','text'),\
    SQLField('blobf','blob'),\
    SQLField('uploadf','upload'),\
    SQLField('integerf','integer'),\
    SQLField('doublef','double'),\
    SQLField('datef','date',default=datetime.date.today()),\
    SQLField('timef','time'),\
    SQLField('datetimef','datetime'),\
    migrate='test_user.table')

A field is an object of type SQLField

>>> SQLField('fieldname', 'fieldtype', length=32,\
             default=None,required=False,requires=[])

Drop the table

>>> db.users.drop()

Examples of insert, select, update, delete

>>> tmp=db.define_table('person',\
          SQLField('name'), \
          SQLField('birth','date'),\
          migrate='test_person.table')
>>> person_id=db.person.insert(name="Marco",birth='2005-06-22')
>>> person_id=db.person.insert(name="Massimo",birth='1971-12-21')
>>> rows=db().select(db.person.ALL)
>>> for row in rows: print row.name
     Marco
Massimo
>>> me=db(db.person.id==person_id).select()[0]
>>> me.name
'Massimo'
>>> db(db.person.name=='Massimo').update(name='massimo')
>>> db(db.person.name=='Marco').delete() # test delete

Update a single record

>>> me.update_record(name="Max")
>>> me.name
'Max'

Complex search conditions

>>> rows=db((db.person.name=='Max')&\
            (db.person.birth<'2003-01-01')).select()
>>> rows=db((db.person.name=='Max')| \
            (db.person.birth<'2003-01-01')).select()
>>> me=db(db.person.id==person_id).select(db.person.name)[0]
>>> me.name
'Max'
>>> rows=db(db.person.birth.month()==12).select()
>>> rows=db(db.person.birth.year()>1900).select()
>>> rows=db(db.person.birth==None).select()
>>> rows=db(db.person.birth!=None).select()
>>> rows=db(db.person.name.upper()=='MAX').select()
>>> rows=db(db.person.name.like('%ax')).select()
>>> rows=db(db.person.name.upper().like('%AX')).select()
>>> rows=db(~db.person.name.upper().like('%AX')).select()

Usage of orderby, groupby and limitby

>>> people=db().select(db.person.name,orderby=db.person.name)
>>> order=db.person.name|~db.person.birth
>>> people=db().select(db.person.name,orderby=order)
>>> people=db().select(db.person.name,orderby=order,\
                       groupby=db.person.name)
>>> people=db().select(db.person.name,orderby=order,limitby=(0,100))

Example of one to many relation

>>> tmp=db.define_table('dog', \
          SQLField('name'), \
          SQLField('birth','date'), \
          SQLField('owner',db.person),\
          migrate='test_dog.table')
>>> dog_id=db.dog.insert(name='Snoopy',birth=None,owner=person_id)

A simple JOIN

>>> rows=db(db.dog.owner==db.person.id).select()
>>> for row in rows: print row.person.name,row.dog.name
Max Snoopy

Example of many to many relation

>>> tmp=db.define_table('author',SQLField('name'),\
                        migrate='test_author.table')
>>> tmp=db.define_table('paper',SQLField('title'),\
                        migrate='test_paper.table')
>>> tmp=db.define_table('authorship',\
        SQLField('author_id',db.author),\
        SQLField('paper_id',db.paper),\
        migrate='test_authorship.table')
>>> aid=db.author.insert(name='Massimo')
>>> pid=db.paper.insert(title='QCD')
>>> tmp=db.authorship.insert(author_id=aid,paper_id=pid)

SQLSet

>>> authored_papers=db((db.author.id==db.authorship.author_id)&\
                       (db.paper.id==db.authorship.paper_id))
>>> rows=authored_papers.select(db.author.name,db.paper.title)
>>> for row in rows: print row.author.name, row.paper.title
Massimo QCD

Search with belongs

>>> set=(1,2,3)
>>> rows=db(db.paper.id.belongs(set)).select(db.paper.ALL)
>>> print rows[0].title
QCD

Nested selects

>>> nested_select=db()._select(db.authorship.paper_id)
>>> rows=db(db.paper.id.belongs(nested_select)).select(db.paper.ALL)
>>> print rows[0].title
QCD

Output in CSV format

>>> str(authored_papers.select(db.author.name,db.paper.title))
'author.name,paper.title\r\nMassimo,QCD\r\n'