[ login ]

Chapter Table of Contents

Services

Web Services

The W3C defines a web service as "a software system designed to support interoperable machine-to-machine interaction over a network". This is a broad definition, and it encompass a large number of protocols designed not for machine-to-human communication, but for machine-to-machine communication such as XML, JSON, RSS, etc.

web2py provides, out of the box, support for many protocols, including XML, JSON, RSS, CSV, XMLRPC, JSONRPC, AMFRPC, and SOAP. web2py can also be extended to support additional protocols.

Each of those protocols is supported in multiple ways, and we make a distinction between:

  • Rendering the output of a function in a given format (for example XML, JSON, RSS, CSV)
  • Remote Procedure Calls (for example XMLRPC, JSONRPC, AMFRPC)

Rendering a dictionary

HTML, XML, and JSON

XML
JSON

Consider the following action:

1.
2.
3.
def count():
session.counter = (session.counter or 0) + 1
return dict(counter=session.counter, now=request.now)

This action returns a counter that is increased by one when a visitor reloads the page, and the timestamp of the current page request.

Normally this page would be requested via:

http://127.0.0.1:8000/app/default/count

and rendered in HTML. Without writing one line of code, we can ask web2py to render this page using a different protocols by adding an extension to the URL:

http://127.0.0.1:8000/app/default/count.html
http://127.0.0.1:8000/app/default/count.xml
http://127.0.0.1:8000/app/default/count.json

The dictionary returned by the action will be rendered in HTML, XML and JSON, respectively.

Here is the XML output:

1.
2.
3.
4.
<document>
<counter>3</counter>
<now>2009-08-01 13:00:00</now>
</document>

Here is the JSON output:

1.
{ 'counter':3, 'now':'2009-08-01 13:00:00' }

Notice that date, time, and datetime objects are rendered as strings in ISO format. This is not part of the JSON standard, but rather a web2py convention.

Generic Views

When, for example, the ".xml" extension is called, web2py looks for a template file called "default/count.xml", and if it does not find it, looks for a template called "generic.xml". The files "generic.html, "generic.xml", "generic.json" are provided with the current scaffolding application. Other extensions can be easily defined by the user.

Nothing needs to be done to enable this in a web2py app. To use it in an older web2py app, you may need to copy the "generic.*" files from a later scaffolding app (after version 1.60).

Here is the code for "generic.html"

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
{{extend 'layout.html'}}

{{=BEAUTIFY(response._vars)}}

<button onclick="document.location='{{=URL("admin","default","design",
args=request.application)}}'">admin</button>
<button onclick="jQuery('#request').slideToggle()">request</button>
<div class="hidden" id="request"><h2>request</h2>{{=BEAUTIFY(request)}}</div>
<button onclick="jQuery('#session').slideToggle()">session</button>
<div class="hidden" id="session"><h2>session</h2>{{=BEAUTIFY(session)}}</div>
<button onclick="jQuery('#response').slideToggle()">response</button>
<div class="hidden" id="response"><h2>response</h2>{{=BEAUTIFY(response)}}</div>
<script>jQuery('.hidden').hide();</script>

Here is the code for "generic.xml"

1.
2.
3.
4.
5.
6.
7.
8.
{{
try:
from gluon.serializers import xml
response.write(xml(response._vars),escape=False)
response.headers['Content-Type']='text/xml'
except:
raise HTTP(405,'no xml')
}}

And here is the code for "generic.json"

1.
2.
3.
4.
5.
6.
7.
8.
{{
try:
from gluon.serializers import json
response.write(json(response._vars),escape=False)
response.headers['Content-Type']='text/json'
except:
raise HTTP(405,'no json')
}}

Any dictionary can be rendered in HTML, XML and JSON as long as it only contains python primitive types (int, float, string, list, tuple, dictionary). response._vars contains the dictionary returned by the action.

If the dictionary contains other user-defined or web2py-specific objects, they must be rendered by a custom view.

Rendering Rows

as_list

If you need to render a set of Rows as returned by a select in XML or JSON or another format, first transform the Rows object into a list of dictionaries using the as_list() method.

Consider for example the following mode:

1.
db.define_table('person', Field('name'))

The following action can be rendered in HTML, but not in XML or JSON:

1.
2.
3.
def everybody():
people = db().select(db.person.ALL)
return dict(people=people)

while the following action can rendered in XML and JSON:

1.
2.
3.
def everybody():
people = db().select(db.person.ALL).as_list()
return dict(people=people)

Custom Formats

If, for example, you want to render an action as a Python pickle:

http://127.0.0.1:8000/app/default/count.pickle

you just need to create a new view file "default/count.pickle" that contains:

1.
2.
3.
4.
5.
{{
import cPickle
response.headers['Content-Type'] = 'application/python.pickle'
response.write(cPickle.dumps(response._vars),escape=False)
}}

If you want to be able to render any action as a pickled file, you need only to save the above file with the name "generic.pickle".

Not all objects are pickleable, and not all pickled objects can be un-pickled. It is safe to stick to primitive Python objects and combinations of them. Objects that do not contain references to file streams or database connections are usually pickleable, but they can only be un-pickled in an environment where the classes of all pickled objects are already defined.

RSS

RSS

web2py includes a "generic.rss" view that can render the dictionary returned by the action as an RSS feed.

Because the RSS feeds have a fixed structure (title, link, description, items, etc.) then for this to work, the dictionary returned by the action must have the proper structure:

1.
2.
3.
4.
5.
{'title'      : ",
'link' : ",
'description': ",
'created_on' : ",
'entries' : []}

and each entry in entries must have the same similar structure:

1.
2.
3.
4.
{'title'      : ",
'link' : ",
'description': ",
'created_on' : "}

For example the following action can be rendered as an RSS feed:

1.
2.
3.
4.
5.
6.
7.
8.
9.
def feed():
return dict(title="my feed",
link="http://feed.example.com",
description="my first feed",
entries=[
dict(title="my feed",
link="http://feed.example.com",
description="my first feed")
])

by simply visiting the URL:

http://127.0.0.1:8000/app/default/feed.rss

Alternatively, assuming the following model:

1.
2.
3.
4.
5.
db.define_table('rss_entry',
Field('title'),
Field('link'),
Field('created_on','datetime'),
Field('description'))

the following action can also be rendered as an RSS feed:

1.
2.
3.
4.
5.
def feed():
return dict(title="my feed",
link="http://feed.example.com",
description="my first feed",
entries=db().select(db.rss_entry.ALL).as_list())

The as_list() method of a Rows object converts the rows into a list of dictionaries.

If additional dictionary items are found with key names not explicitly listed here, they are ignored.

Here is the "generic.rss" view provided by web2py:

1.
2.
3.
4.
5.
6.
7.
8.
{{
try:
from gluon.serializers import rss
response.write(rss(response._vars),escape=False)
response.headers['Content-Type']='application/rss+xml'
except:
raise HTTP(405,'no rss')
}}

As one more example of an RSS application, we consider an RSS aggregator that collects data from the "slashdot" feed and returns a new web2py rss feed.

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
def aggregator():
import gluon.contrib.feedparser as feedparser
d = feedparser.parse(
"http://rss.slashdot.org/Slashdot/slashdot/to")
return dict(title=d.channel.title,
link = d.channel.link,
description = d.channel.description,
created_on = request.now,
entries = [
dict(title = entry.title,
link = entry.link,
description = entry.description,
created_on = request.now) for entry in d.entries])

It can be accessed at:

http://127.0.0.1:8000/app/default/aggregator.rss

CSV

CSV

The Comma Separated Values (CSV) format is a protocol to represent tabular data.

Consider the following model:

1.
2.
3.
4.
db.define_model('animal',
Field('species'),
Field('genus'),
Field('family'))

and the following action:

1.
2.
3.
def animals():
animals = db().select(db.animal.ALL)
return dict(animals=animals)

web2py does not provide a "generic.csv"; you must define a custom view "default/animals.csv" that serializes the animals into CSV. Here is a possible implementation:

1.
2.
3.
4.
5.
6.
7.
{{
import cStringIO
stream=cStringIO.StringIO()
animals.export_to_csv_file(stream)
response.headers['Content-Type']='application/vnd.ms-excel'
response.write(stream.getvalue(), escape=False)
}}

Notice that one could also define a "generic.csv" file, but one would have to specify the name of the object to be serialized ("animals" in the example). This is why we do not provide a "generic.csv" file.

Remote Procedure Calls

RPC

web2py provides a mechanism to turn any function into a web service. The mechanism described here differs from the mechanism described before because:

  • The function may take arguments
  • The function may be defined in a model or a module instead of controller
  • You may want to specify in detail which RPC method should be supported
  • It enforces a more strict URL naming convention
  • It is smarter then the previous methods because it works for a fixed set of protocols. For the same reason it is not as easily extensible.

To use this feature:

First, you must import and instantiate a service object.

1.
2.
from gluon.tools import Service
service = Service(globals())

This is already done in the "db.py" model file in the scaffolding application.
Second, you must expose the service handler in the controller:
1.
2.
3.
def call():
session.forget()
return service()
This is already done in the "default.py" controller of the scaffolding application. Remove session.forget() if you plan to use session cookies with the services.
Third, you must decorate those functions you want to expose as a service. Here is a list of currently supported decorators:
1.
2.
3.
4.
5.
6.
7.
8.
9.
@service.run
@service.xml
@service.json
@service.rss
@service.csv
@service.xmlrpc
@service.jsonrpc
@service.amfrpc3('domain')
@service.soap('FunctionName',returns={'result':type},args={'param1':type,})

As an example, consider the following decorated function:

1.
2.
3.
@service.run
def concat(a,b):
return a+b

This function can be defined in a model or in the controller where the call action is defined. This function can now be called remotely in two ways:

http://127.0.0.1:8000/app/default/call/run/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/run/concat/hello/world

In both cases the http request returns:

1.
helloworld

If the @service.xml decorator is used, the function can be called via:

http://127.0.0.1:8000/app/default/call/xml/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/xml/concat/hello/world

and the output is returned as XML:

1.
2.
3.
<document>
<result>helloworld</result>
</document>

It can serialize the output of the function even if this is a DAL Rows object. In this case, in fact, it will call as_list() automatically.

If the @service.json decorator is used, the function can be called via:

http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/json/concat/hello/world

and the output returned as JSON.

If the @service.csv decorator is used, the service handler requires, as the return value, an iterable object of iterable objects, such as a list of lists. Here is an example:

1.
2.
3.
@service.csv
def table1(a,b):
return [[a,b],[1,2]]

This service can be called by visiting one of the following URLs:

http://127.0.0.1:8000/app/default/call/csv/table1?a=hello&b=world
http://127.0.0.1:8000/app/default/call/csv/table1/hello/world

and it returns:

1.
2.
hello,world
1,2

The @service.rss decorator expects a return value in the same format as the "generic.rss" view discussed in the previous section.

Multiple decorators are allowed for each function.

So far, everything discussed in this section is simply an alternative to the method described in the previous section. The real power of the service object comes with XMLRPC, JSONRPC and AMFRPC, as discussed below.

XMLRPC

XMLRPC

Consider the following code, for example, in the "default.py" controller:

1.
2.
3.
4.
5.
6.
7.
@service.xmlrpc
def add(a,b):
return a+b

@service.xmlrpc
def div(a,b):
return a/b

Now in a python shell you can do

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>> from xmlrpclib import ServerProxy
>>> server = ServerProxy(
'http://127.0.0.1:8000/app/default/call/xmlrpc')
>>>
print server.add(3,4)
7
>>> print server.add('hello','world')
'helloworld'
>>> print server.div(12,4)
3
>>> print server.div(1,0)
ZeroDivisionError: integer division or modulo by zero

The Python xmlrpclib module provides a client for the XMLRPC protocol. web2py acts as the server.

The client connects to the server via ServerProxy and can remotely call decorated functions in the server. The data (a,b) is passed to the function(s), not via GET/POST variables, but properly encoded in the request body using the XMLPRC protocol, and thus it carries with itself type information (int or string or other). The same is true for the return value(s). Moreover, any exception raised on the server propagates back to the client.

There are XMLRPC libraries for many programming languages (including C, C++, Java, C#, Ruby, and Perl), and they can interoperate with each other. This is one the best methods to create applications that talk to each other independent of the programming language.

The XMLRPC client can also be implemented inside a web2py action, so that one action can talk to another web2py application (even within the same installation) using XMLRPC. Beware of session deadlocks in this case. If an action calls via XMLRPC a function in the same app, the caller must release the session lock before the call:

1.
2.
session.forget()
session._unlock(response)

JSONRPC and Pyjamas

JSONRPC
Pyjamas

JSONRPC is very similar to XMLRPC, but uses the JSON-based protocol instead of XML to encode the data. As an example of application here, we discuss its usage with Pyjamas. Pyjamas is a Python port of the Google Web Toolkit (originally written in Java). Pyjamas allows writing a client application in Python. Pyjamas translates this code into JavaScript. web2py serves the JavaScript and communicates with it via AJAX requests originating from the client and triggered by user actions.

Here we describe how to make Pyjamas work with web2py. It does not require any additional libraries other than web2py and Pyjamas.

We are going to build a simple "todo" application with a Pyjamas client (all JavaScript) that talks to the server exclusively via JSONRPC.

First, create a new application called "todo".

Second, in "models/db.py", enter the following code:

1.
2.
3.
4.
5.
db=SQLDB('sqlite://storage.sqlite')
db.define_table('todo', Field('task'))

from gluon.tools import Service # import rpc services
service = Service(globals())

Third, in "controllers/default.py", enter the following code:

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
def index():
redirect(URL('todoApp'))

@service.jsonrpc
def getTasks():
todos = db(db.todo.id>0).select()
return [(todo.task,todo.id) for todo in todos]

@service.jsonrpc
def addTask(taskFromJson):
db.todo.insert(task= taskFromJson)
return getTasks()

@service.jsonrpc
def deleteTask (idFromJson):
del db.todo[idFromJson]
return getTasks()

def call():
session.forget()
return service()

def todoApp():
return dict()

The purpose of each function should be obvious.

Fourth, in "views/default/todoApp.html", enter the following code:

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
<html>
<head>
<meta name="pygwt:module"
content="{{=URL('static','output/todoapp')}}" />
<title>
simple todo application
</title>
</head>
<body bgcolor="white">
<h1>
simple todo application
</h1>
<i>
type a new task to insert in db,
click on existing task to delete it
</i>
<script language="javascript"
src="{{=URL('static','output/pygwt.js')}}">
</script>

</body>
</html>

This view just executes the Pyjamas code in "static/output/todoapp" - code that we have not yet created.

Fifth, in "static/TodoApp.py" (notice it is TodoApp, not todoApp!), enter the following client code:

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.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.TextBox import TextBox
import pyjamas.ui.KeyboardListener
from pyjamas.ui.ListBox import ListBox
from pyjamas.ui.HTML import HTML
from pyjamas.JSONService import JSONProxy

class TodoApp:
def onModuleLoad(self):
self.remote = DataService()
panel = VerticalPanel()

self.todoTextBox = TextBox()
self.todoTextBox.addKeyboardListener(self)

self.todoList = ListBox()
self.todoList.setVisibleItemCount(7)
self.todoList.setWidth("200px")
self.todoList.addClickListener(self)
self.Status = Label("")

panel.add(Label("Add New Todo:"))
panel.add(self.todoTextBox)
panel.add(Label("Click to Remove:"))
panel.add(self.todoList)
panel.add(self.Status)
self.remote.getTasks(self)

RootPanel().add(panel)

def onKeyUp(self, sender, keyCode, modifiers):
pass

def
onKeyDown(self, sender, keyCode, modifiers):
pass

def
onKeyPress(self, sender, keyCode, modifiers):
"""
This function handles the onKeyPress event, and will add the
item in the text box to the list when the user presses the
enter key. In the future, this method will also handle the
auto complete feature.
"""
if keyCode == KeyboardListener.KEY_ENTER and \
sender == self.todoTextBox:
id = self.remote.addTask(sender.getText(),self)
sender.setText("")
if id<0:
RootPanel().add(HTML("Server Error or Invalid Response"))

def onClick(self, sender):
id = self.remote.deleteTask(
sender.getValue(sender.getSelectedIndex()),self)
if id<0:
RootPanel().add(
HTML("Server Error or Invalid Response"))

def onRemoteResponse(self, response, request_info):
self.todoList.clear()
for task in response:
self.todoList.addItem(task[0])
self.todoList.setValue(self.todoList.getItemCount()-1,
task[1])

def onRemoteError(self, code, message, request_info):
self.Status.setText("Server Error or Invalid Response: " \
+ "ERROR " + code + " - " + message)

class DataService(JSONProxy):
def __init__(self):
JSONProxy.__init__(self, "../../default/call/jsonrpc",
[
"getTasks", "addTask","deleteTask"])

if __name__ == '__main__':
app = TodoApp()
app.onModuleLoad()

Sixth, run Pyjamas before serving the application:

1.
2.
cd /path/to/todo/static/
python /python/pyjamas-0.5p1/bin/pyjsbuild TodoApp.py

This will translate the Python code into JavaScript so that it can be executed in the browser.

To access this application, visit the URL:

http://127.0.0.1:8000/todo/default/todoApp

This subsection was created by Chris Prinos with help from Luke Kenneth Casson Leighton (creators of Pyjamas), updated by Alexei Vinidiktov. It has been tested with Pyjamas 0.5p1. The example was inspired by this Django page in ref.74.

AMFRPC

PyAMF
Adobe Flash

AMFRPC is the Remote Procedure Call protocol used by Flash clients to communicate with a server. web2py supports AMFRPC, but it requires that you run web2py from source and that you preinstall the PyAMF library. This can be installed from the Linux or Windows shell by typing:

1.
easy_install pyamf

(please consult the PyAMF documentation for more details).

In this subsection we assume that you are already familiar with ActionScript programming.

We will create a simple service that takes two numerical values, adds them together, and returns the sum. We will call our web2py application "pyamf_test", and we will call the service addNumbers.

First, using Adobe Flash (any version starting from MX 2004), create the Flash client application by starting with a new Flash FLA file. In the first frame of the file, add these lines:

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.
import mx.remoting.Service;
import mx.rpc.RelayResponder;
import mx.rpc.FaultEvent;
import mx.rpc.ResultEvent;
import mx.remoting.PendingCall;

var val1 = 23;
var val2 = 86;

service = new Service(
"http://127.0.0.1:8000/pyamf_test/default/call/amfrpc3",
null, "mydomain", null, null);

var pc:PendingCall = service.addNumbers(val1, val2);
pc.responder = new RelayResponder(this, "onResult", "onFault");

function onResult(re:ResultEvent):Void {
trace("Result : " + re.result);
txt_result.text = re.result;
}

function onFault(fault:FaultEvent):Void {
trace("Fault: " + fault.fault.faultstring);
}

stop();

This code allows the Flash client to connect to a service that corresponds to a function called "addNumbers" in the file "/pyamf_test/default/gateway". You must also import ActionScript version 2 MX remoting classes to enable Remoting in Flash. Add the path to these classes to the classpath settings in the Adobe Flash IDE, or just place the "mx" folder next to the newly created file.

Notice the arguments of the Service constructor. The first argument is the URL corresponding to the service that we want will create. The third argument is the domain of the service. We choose to call this domain "mydomain".

Second, create a dynamic text field called "txt_result" and place it on the stage.

Third, you need to set up a web2py gateway that can communicate with the Flash client defined above.

Proceed by creating a new web2py app called pyamf_test that will host the new service and the AMF gateway for the flash client. Edit the "default.py" controller and make sure it contains

1.
2.
3.
4.
5.
@service.amfrpc3('mydomain')
def addNumbers(val1, val2):
return val1 + val2

def call(): return service()

Fourth, compile and export/publish the SWF flash client as pyamf_test.swf, place the "pyamf_test.amf", "pyamf_test.html", "AC_RunActiveContent.js", and "crossdomain.xml" files in the "static" folder of the newly created appliance that is hosting the gateway, "pyamf_test".

You can now test the client by visiting:

http://127.0.0.1:8000/pyamf_test/static/pyamf_test.html

The gateway is called in the background when the client connects to addNumbers.

If you are suing AMF0 instead of AMF3 you can also use the decorator:

1.
@service.amfrpc

instead of:

1.
@service.amfrpc3('mydomain')

In this case you also need to change the service URL to:

http://127.0.0.1:8000/pyamf_test/default/call/amfrpc

SOAP

SOAP

web2py includes a SOAP client and server created by Mariano Reingart. It can be used very much like XML-RPC:

Consider the following code, for example, in the "default.py" controller:

1.
2.
3.
@service.soap('MyAdd',returns={'result':int},args={'a':int,'b':int,})
def add(a,b):
return a+b

Now in a python shell you can do:

1.
2.
3.
4.
>>> from gluon.contrib.pysimplesoap.client import SoapClient
>>> client = SoapClient(wsdl="http://localhost:8000/app/default/call/soap?WSDL")
>>>
print client.MyAdd(a=1,b=2)
{
'result': 3}

To get proper encoding when returning a text values, specify string as u'proper utf8 text'.

You can obtain the WSDL for the service at

http://127.0.0.1:8000/app/default/call/soap?WSDL

And you can obtain documentation for any of the exposed methods:

http://127.0.0.1:8000/app/default/call/soap

Low Level API and Other Recipes

simplejson

JSON
simplejson

web2py includes gluon.contrib.simplejson, developed by Bob Ippolito. This module provides the most standard Python-JSON encoder-decoder.

SimpleJSON consists of two functions:

  • gluon.contrib.simplesjson.dumps(a) encodes a Python object a into JSON.
  • gluon.contrib.simplejson.loads(b) decodes a JavaScript object b into a Python object.

Object types that can be serialized include primitive types, lists, and dictionaries. Compound objects can be serialized with the exception of user defined classes.

Here is a sample action (for example in controller "default.py") that serializes the Python list containing weekdays using this low level API:

1.
2.
3.
4.
5.
def weekdays():
names=['Sunday','Monday','Tuesday','Wednesday',
'Thursday','Friday','Saturday']
import gluon.contrib.simplejson
return gluon.contrib.simplejson.dumps(names)

Below is a sample HTML page that sends an Ajax request to the above action, receives the JSON message and stores the list in a corresponding JavaScript variable:

1.
2.
3.
4.
5.
{{extend 'layout.html'}}
<script>
$.getJSON('/application/default/weekdays',
function(data){ alert(data); });
</script>

The code uses the jQuery function $.getJSON, which performs the Ajax call and, on response, stores the weekdays names in a local JavaScript variable data and passes the variable to the callback function. In the example the callback function simply alerts the visitor that the data has been received.

PyRTF

PyRTF
RTF

Another common need of web sites is that of generating Word-readable text documents. The simplest way to do so is using the Rich Text Format (RTF) document format. This format was invented by Microsoft and it has since become a standard.

web2py includes gluon.contrib.pyrtf, developed by Simon Cusack and revised by Grant Edwards. This module allows you to generate RTF documents programmatically including colored formatted text and pictures.

In the following example we instantiate two basic RTF classes, Document and Section, append the latter to the former and insert some dummy text in the latter:

1.
2.
3.
4.
5.
6.
7.
8.
9.
def makertf():
import gluon.contrib.pyrtf as q
doc=q.Document()
section=q.Section()
doc.Sections.append(section)
section.append('Section Title')
section.append('web2py is great. '*100)
response.headers['Content-Type']='text/rtf'
return q.dumps(doc)

In the end the Document is serialized by q.dumps(doc). Notice that before returning an RTF document it is necessary to specify the content-type in the header else the browser does not know how to handle the file.

Depending on the configuration, the browser may ask you whether to save this file or open it using a text editor.

ReportLab and PDF

ReportLab
PDF

web2py can also generate PDF documents, with an additional library called "ReportLab"73 .

If you are running web2py from source, it is sufficient to have ReportLab installed. If you are running the Windows binary distribution, you need to unzip ReportLab in the "web2py/" folder. If you are running the Mac binary distribution, you need to unzip ReportLab in the folder:

1.
web2py.app/Contents/Resources/

From now on we assume ReportLab is installed and that web2py can find it. We will create a simple action called "get_me_a_pdf" that generates a PDF document.

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.
from reportlab.platypus import *
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.lib import colors
from uuid import uuid4
from cgi import escape
import os

def get_me_a_pdf():
title = "This The Doc Title"
heading = "First Paragraph"
text = 'bla '* 10000

styles = getSampleStyleSheet()
tmpfilename=os.path.join(request.folder,'private',str(uuid4()))
doc = SimpleDocTemplate(tmpfilename)
story = []
story.append(Paragraph(escape(title),styles["Title"]))
story.append(Paragraph(escape(heading),styles["Heading2"]))
story.append(Paragraph(escape(text),styles["Normal"]))
story.append(Spacer(1,2*inch))
doc.build(story)
data = open(tmpfilename,"rb").read()
os.unlink(tmpfilename)
response.headers['Content-Type']='application/pdf'
return data

Notice how we generate the PDF into a unique temporary file, tmpfilename, we read the generated PDF from the file, then we deleted the file.

For more information about the ReportLab API, refer to the ReportLab documentation. We strongly recommend using the Platypus API of ReportLab, such as Paragraph, Spacer, etc.

Services and Authentication

Authentication

In the previous chapter we have discussed the use of the following decorators:

1.
2.
3.
@auth.requires_login()
@auth.requires_membership(...)
@auth.requires_permission(...)

For normal actions (not decorated as services), these decorators can be used even if the output is rendered in a format other than HTML.

For functions defined as services and decorated using the @service... decorators, the @auth... decorators should not be used. The two types of decorators cannot be mixed. If authentication is to be performed, it is the call actions that needs to be decorated:

1.
2.
@auth.requires_login()
def call(): return service()

Notice that it also possible to instantiate multiple service objects, register the same different functions with them, and expose some of them with authentication and some not:

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
public_services=Service(globals())
private_services=Service(globals())

@public_service.jsonrpc
@private_service.jsonrpc
def f(): return 'public'

@private_service.jsonrpc
def g(): return 'private'

def public_call(): return public_service()

@auth.requires_login()
def private_call(): return private_service()

This assumes that the caller is passing credentials in the HTTP header (a valid session cookie or using basic authentication, as discussed in the previous section). The client must support it; not all clients do.

© 2008-2010 by Massimo Di Pierro - All rights reserved - Powered by web2py - design derived from a theme by the earlybird
The content of this book is released under the Artistic License 2.0 - Modified content cannot be reproduced.