Web2py:
Web development like a boss
Francisco Gama Tabanez Ribeiro
Agenda
‣ my path on web dev - lessons learned
‣ web2py web framework:
‣ intro
‣ internals
‣ live demo - building a WiKi
‣ resources
view source
1999 - Lisbon,
Portugal
1
0 1 11 01
01
0 0
1
5 years later...
PHP?
Python is well designed, PHP is not:
Although it’s perfectly possible to write good
code in PHP,
it’s much easier to write great code in Python
from Why PHP Is Fun and Easy But Python Is Marriage
Material
Perl?
• LUKE: Is Perl better than Python?
• YODA: No... no... no. Quicker, easier, more seductive.
• LUKE: But how will I know why Python is better than Perl?
• YODA:You will know. . .
When your code you try to read six months from now.
Ruby?
Beautiful is better than ugly.
[...]
Although practicality beats purity.
from The Zen of Python
?
?
lessons learned (1/2)
• rapid turnaround
• secure by design
• web development is mostly about visual UI’s
• relies mostly on user interaction & feedback
• convention over configuration
• flexible as in hackable
lessons learned (2/2)
• fast and efficient but mostly scalable and stable
• different things come in different places
• plays well with others
• backward compatibility assured
• live community & support resources
• fun
Welcome to the club!
founder: Massimo Di Pierro
main contributors: +80
users: +2000
Install and run...
Install and run...
curl -O http://www.web2py.com/examples/static/web2py_src.zip
why?
• easy to run, install and play with
• python based
• web IDE included
• web based database admin
• no dependencies, no configuration required
• multiple platform (GAE, EC2, Jython...)
why?
• Open source (LGPLv3 license)
• web2py applications have no license constraints
• web2py allows application bytecode compilation
• always backward compatible
• deployment-friendly...
deployment..
• always backward
compatible
• integrates well with web
servers (cgi, fcgi,
mod_python, mod_wsgi)
• error logging and
ticketing support
• Database Abstraction
Layer integrated
• integrated versioning
• integrated self-update
capability
• multiple caching
methods
• testing methods and
debug shell
what else?
• secure (against XSS, Injection flaws, RFI)
• SSL and multiple authentication methods
• enforces good Software Engineering practices (MVC,
Server-side form validation, postbacks...)
• Internationalization support
• HTML/XML, RSS/ATOM, RTF, PDF, JSON, AJAX
(includes jQuery), XML-RPC, CSV, REST, WIKI, Flash/
AMF, and Linked Data (RDF)
Web based
Admin Interface
Hello World
def hello1():
return "Hello World"
controllers/simple_examples.py
Olá Mundo
(Translation)
def hello2():
return T("Hello World")
controllers/simple_examples.py
Controller-View
def hello3():
return dict(message=T("Hello World"))
{{extend 'layout.html'}}
<h1>{{=message}}</h1>
controllers/simple_examples.py
views/hello3.html
View
{{extend 'layout.html'}}
<h1>{{=message}}</h1>
<html>
<head>
<title>example</title>
</head>
<body>
{{include}}
</body>
</html>
views/
views/hello3.html
Web2py: flow
Web2py: inside
Web2py: inside
Python
Web2py: inside
Python
rocket
(SSL enabled web server)
API for third party servers
(Apache,...)
Web2py: inside
Python
rocket
(SSL enabled web server)
API for third party servers
(Apache,...)
Core libraries
(HTTP request/response, session,
auth, services, DAL,...)
Contrib
(simplejson, markdown,
memcache, pyrtf, rss,...)
Web2py: inside
Python
rocket
(SSL enabled web server)
API for third party servers
(Apache,...)
Core libraries
(HTTP request/response, session,
auth, services, DAL,...)
User Applications
admin welcome user app
Contrib
(simplejson, markdown,
memcache, pyrtf, rss,...)
examples
Web2py: inside
Python
rocket
(SSL enabled web server)
API for third party servers
(Apache,...)
Core libraries
(HTTP request/response, session,
auth, services, DAL,...)
User Applications
admin welcome user app
Contrib
(simplejson, markdown,
memcache, pyrtf, rss,...)
examples
web2py app inside
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
models
controllers
views
translations
static data
plugins & modules
appadmin
data
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
db = SQLDB(‘sqlite://data.db’)
db.define_table(‘category’,
Field(‘name’))
db.define_table(‘recipe’,
Field(‘title’),
Field(‘category’,db.category),
Field(‘description’,‘text’))
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
db = SQLDB(‘sqlite://data.db’)
db.define_table(‘category’,
Field(‘name’))
db.define_table(‘recipe’,
Field(‘title’),
Field(‘category’,db.category),
Field(‘description’,‘text’))
SQlite
MySQL
PostgreSQL
Oracle
MSSQL
DB2
Firebird
MyBase
Informix
Google App Engine
Database types:
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
db = SQLDB(‘sqlite://data.db’)
db.define_table(‘category’,
Field(‘name’))
db.define_table(‘recipe’,
Field(‘title’),
Field(‘category’,db.category),
Field(‘description’,‘text’))
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
db = SQLDB(‘sqlite://data.db’)
db.define_table(‘category’,
Field(‘name’))
db.define_table(‘recipe’,
Field(‘title’),
Field(‘category’,db.category),
Field(‘description’,‘text’))
string
text
integer
double
date
datetime
time
boolean
password
upload
blob
reference
list:string
list:interger
list:reference
Field types:
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
db = SQLDB(‘sqlite://data.db’)
db.define_table(‘category’,
Field(‘name’))
db.define_table(‘recipe’,
Field(‘title’),
Field(‘category’,db.category),
Field(‘description’,‘text’))
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
db = SQLDB(‘sqlite://data.db’)
db.define_table(‘category’,
Field(‘name’))
db.define_table(‘recipe’,
Field(‘title’),
Field(‘category’,db.category),
Field(‘description’,‘text’))
db.recipe.title.requires=IS_NOT_EMPTY()
db.recipe.category.requires=IS_IN_DB(db,'category.id','category.name')
db.category.name.requires=IS_NOT_IN_DB(db,'category.name')
db.recipe.description.default='Fill your recipe in here...'
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
db = SQLDB(‘sqlite://data.db’)
db.define_table(‘category’,
Field(‘name’))
db.define_table(‘recipe’,
Field(‘title’),
Field(‘category’,db.category),
Field(‘description’,‘text’))
db.recipe.title.requires=IS_NOT_EMPTY()
db.recipe.category.requires=IS_IN_DB(db,'category.id','category.name')
db.category.name.requires=IS_NOT_IN_DB(db,'category.name')
db.recipe.description.default='Fill your recipe in here...'
IS_DATE
IS_DATETIME
IS_DATETIME_IN_RANGE
IS_DATE_IN_RANGE
IS_DECIMAL_IN_RANGE
IS_EMAIL
IS_EMPTY_OR
IS_EQUAL_TO
IS_EXPR
IS_FLOAT_IN_RANGE
IS_GENERIC_URL
IS_HTTP_URL
IS_IMAGE
IS_INT_IN_RANGE
IS_IN_DB
IS_IN_SET
IS_IN_SUBSET
IS_IPV4
IS_LENGTH
IS_LIST_OF
IS_LOWER
IS_MATCH
IS_NOT_EMPTY
IS_NOT_IN_DB
IS_NULL_OR
IS_SLUG
IS_STRONG
IS_TIME
IS_UPLOAD_FILENAME
IS_UPPER
IS_URL
IS_ALPHANUMERIC
Validators:
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
def index():
return dict(recipes=db().select(db.recipe.ALL))
def add():
form=SQLFORM(db.recipe)
if form.accepts(request.vars):
redirect(URL(r=request,f='index'))
return dict(form=form)
def show():
recipes=db(db.recipe.id==request.args[0]).select()
if not len(recipes):
redirect(URL(r=request,f='index'))
return dict(recipe=recipes[0])
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
in file views/default/add.html:
{{extend 'layout.html'}}
<h1>{{T(‘New recipe’)}}</h1>
{{=form}}
in file views/default/index.html:
{{extend 'layout.html'}}
<h1> {{T(‘List all recipes’)}} </h1>
<table>
{{for recipe in recipes:}}
<tr><td>{{=A(T(recipe.title),_href=URL(r=request,f='sh
ow', args=recipe.id))}}</td></tr>
{{pass}}
</table>
in file views/default/show.html:
{{extend 'layout.html'}}
<h1> {{=T(recipe.title)}}
{{=db.recipe.category.represent(recipe.category)}}
</h1>
<pre> {{=recipe.description}}</pre>
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
T(‘New recipe’)
T(‘List all recipes’)
T(recipe.title)
‘Nova receita’
‘Nouvelle receité’
...
‘Listar receitas’
‘Lister receité’
...
‘bitoque’
‘duple cliché’
...
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
images
css
js
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
ways to extend your apps:
• modules are good way to import external
code
• plugins are applications subsets - a small
application “inside” your application
web2py app inside
models
controllers
views
translations
static data
plugins & modules
appadmin
data
web2py app inside
• databases (SQlite, MySQL, PostgreSQL,
Oracle, MSSQL, DB2, Firebird, MyBase,
Informix, Google App Engine)
• metadata for automatic migrations
• cache
models
controllers
views
translations
static data
plugins & modules
appadmin
data
default web based interface to your data
web2py app inside
web2py URL parsing
http://www.myhost.com:8000/myapp/default/index.html/a/b/c?name=Max
request.application = “myapp”
request.controller = “default”
request.function = “index”
request.extension = “html”
request.args = [‘a’,’b’,’c’]
request.vars.name = “Max”
Environment variables
are in request.env
equivalent to
request.vars[‘name’]
• functions in controllers return dicts() into views
• controller methods are exposed to views with the
same name which is also used in the URL
• input validators (forms) are defined in the model as
integrity constraints (requires)
• DAL automatically creates the id field in your tables
• model code has full application scope
remember..
remember..
• you can use your editor of choice
• you can use DAL separately
• in controller functions you can redefine the target
view with response.view=”theme/myview.html”
• web2py shell is your friend
• recommended - start your files with:
# coding: utf8
sessions
<html>
<head></head>
<body>
<h1>{{=message}}</h1>
<h2>Number of visits: {{=counter}}</h2>
</body>
</html>
def index():
session.counter = (session.counter or 0) + 1
return dict(message="Hello from MyApp", counter=session.counter)
inside controller:
inside view:
Overview
DAL select()
db(query).select(
field1, field2, ...,
left=[db.table.on(query)],
orderby=field|~field,
groupby=field|field
limitby=(0,10),
cache=(cache.ram,5000))
Examples:
rows = db(db.recipe).select()
rows = db(db.recipe.id>0).select(
db.recipe.title, db.recipe.category,
orderby = db.recipe.category,
distinct=True)
# christmas recipes from 2000
query1 = db.recipe.created_in.year()>2000
query2 = db.recipe_created_in.month()<10
rows = db(query1 & query2).select()
DAL select()
db(query).select(
field1, field2, ...,
left=[db.table.on(query)],
orderby=field|~field,
groupby=field|field
limitby=(0,10),
cache=(cache.ram,5000))
Examples:
rows = db(db.recipe).select()
rows = db(db.recipe.id>0).select(
db.recipe.title, db.recipe.category,
orderby = db.recipe.category,
distinct=True)
# christmas recipes from 2000
query1 = db.recipe.created_in.year()>2000
query2 = db.recipe_created_in.month()<10
rows = db(query1 & query2).select()
equivalent to:
rows = db(db.recipe.id>0).select(db.recipe.ALL)
DAL select()
db(query).select(
field1, field2, ...,
left=[db.table.on(query)],
orderby=field|~field,
groupby=field|field
limitby=(0,10),
cache=(cache.ram,5000))
Examples:
rows = db(db.recipe).select()
rows = db(db.recipe.id>0).select(
db.recipe.title, db.recipe.category,
orderby = db.recipe.category,
distinct=True)
# christmas recipes from 2000
query1 = db.recipe.created_in.year()>2000
query2 = db.recipe_created_in.month()<10
rows = db(query1 & query2).select()
equivalent to:
rows = db(db.recipe.id>0).select(db.recipe.ALL)
queries can be combined with and(&), or(|) and not(~)
DAL operations
• Insert:
db.category.insert(name=‘soup’)
• Update:
db(db.category.name==‘soup’).update(name=‘french soups’)
• Delete:
db(db.category.name==‘french soups’).delete()
• Count:
db(db.category.name.like(‘%soup%’)).count()
DAL operations
• Insert:
db.category.insert(name=‘soup’)
• Update:
db(db.category.name==‘soup’).update(name=‘french soups’)
• Delete:
db(db.category.name==‘french soups’).delete()
• Count:
db(db.category.name.like(‘%soup%’)).count()
Other operators:
.max(), .min(), .sum(), .bel
ongs(), .like(),...
DAL operations
• Insert:
db.category.insert(name=‘soup’)
• Update:
db(db.category.name==‘soup’).update(name=‘french soups’)
• Delete:
db(db.category.name==‘french soups’).delete()
• Count:
db(db.category.name.like(‘%soup%’)).count()
Other operations:
transactions, inner joins, left outer joins, nested selects, self-references, many2many, ...
Other operators:
.max(), .min(), .sum(), .bel
ongs(), .like(),...
Forms
• SQLFORM() / SQLFORM.factory()
• FORM()
• CRUD()
• <form></form>
HTML helpers (form)
def display_form():
form=FORM('Your name:',
INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
if form.accepts(request.vars, session):
response.flash = form.vars.name +', thank you'
elif form.errors:
response.flash = 'form has errors'
else:
response.flash = 'please fill the form'
return dict(form=form)
...
<div class="flash">{{=response.flash or ''}}</div>
{{=form}}
...
inside controller:
inside view:
Components
LOAD()
{{=LOAD(‘controller’,‘function’, ajax=True)}}
Components
LOAD()
{{=LOAD(‘controller’,‘function’, ajax=True)}}
supports auth signatures
web2py Shell
python web2py.py -S myapplication -M
HTML helpers
• BEAUTIFY(whatever)
• URL('application', 'controller', 'function',
args=['x', 'y'], vars=dict(z='t'))
• much more... /application/controller/function/x/y?z=t
Authentication &
Authorization (Role-based)
Authentication &
Authorization (Role-based)
doc_id = db.document.insert(body = 'top secret')
agents = auth.add_group(role = 'Secret Agent')
auth.add_membership(agents, james_bond)
auth.add_permission(agents, 'read', secrets)
auth.has_permission('read', secrets, doc_id, james_bond)
auth.has_permission('update', secrets, doc_id, james_bond)
auth.is_logged_in()
Authentication &
Authorization (Role-based)
@auth.requires_login()
@auth.requires_membership(agents)
@auth.requires_permission('read', secrets)
@auth.requires_permission('delete', 'any file')
@auth.requires(auth.user_id==1 or request.client=='127.0.0.1')
@auth.requires_permission('add', 'number')
you also have:
agents = auth.add_group(role = 'Secret Agent')
auth.add_membership(agents, james_bond)
auth.add_permission(agents, 'read', secrets)
@service.csv
@service.rss
@service.xml
def list_recipes():
return db(db.recipe).select()
@service.run
@service.csv
@service.rss
@service.json
@service.jsonrpc
@service.xml
@service.xmlrpc
@service.soap
@service.amfrpc3('domain')
Services
http://myhost/application/controller/list_recipes.xml
@service.csv
@service.rss
@service.xml
def list_recipes():
return db(db.recipe).select()
@service.run
@service.csv
@service.rss
@service.json
@service.jsonrpc
@service.xml
@service.xmlrpc
@service.soap
@service.amfrpc3('domain')
Services
http://myhost/application/controller/list_recipes.xml
.csv
...
.json
@service.csv
@service.rss
@service.xml
def list_recipes():
return db(db.recipe).select()
@service.run
@service.csv
@service.rss
@service.json
@service.jsonrpc
@service.xml
@service.xmlrpc
@service.soap
@service.amfrpc3('domain')
Services
http://myhost/application/controller/list_recipes.xml
.csv
...
.json
Errors (ticketing)
errors/exceptions are logged into tickets
Other
• Cron
• Routes
• Plugins
• Modules
• Grids
Caching in functions:
@cache("key",cache.ram,5000)
def f(): return dict()
Caching actions/views:
@cache(request.env.path_info,5000,cache.ram)
def action():
return response.render(response.view,dict())
could be:
cache.ram, cache.disk, cache.memcache
demoWiki
demoWiKi app
(features)
• add pages
• show pages
• edit pages
• versioning
• authentication
(local and remote)
• internationalization
• comments
(with/without AJAX)
demoWiKi app
(model)
web2py in real life
Francisco Costa
Documentation (1/2)
• online book (www.web2py.com/book)
• book (printed version - amzn.to/vzjiqT)
• screencasts (www.vimeo.com)
• interactive examples (default app)
• AlterEgo (FAQ - www.web2py.com/AlterEgo)
Documentation (2/2)
• epydoc
(www.web2py.com/examples/static/epydoc/)
• API:
(www.web2py.com/book/default/chapter/
04#API)
Community (1/2)
• free web2py appliances
(www.web2py.com/appliances)
• code snippets (www.web2pyslices.com)
• groups:
• users - http://groups.google.com/group/web2py/
• developers - http://groups.google.com/group/web2py-developers
Community (2/2)
• #web2py (IRC channel at irc.freenode.net)
• twitter (http://twitter.com/web2py)
• user voice (http://web2py.uservoice.com)
• web2py websites (www.web2py.com/
poweredby)
• web2py plugins (www.web2py.com/plugins/)
Thank you
childish wont-let-go nickname: blackthorne
blackthorne (geek)
bthorne_daily (social)
francisco@ironik.org
(PGP key: 0xBDD20CF1)
http://www.digitalloft.org
(homepage)
web2py app defaults
• menu
• web2py_ajax (jQuery)
• auth, mail, download and services
• english default lang translation
• generic view

web2py:Web development like a boss