We will create an addon module for our website features. We can call it todo_website
. To introduce the basics of Odoo web development, we will implement a simple Hello World web page. Imaginative, right?
As usual, we will start creating it's manifest file. Create the todo_website/__manifest__.py
file with:
{ 'name': 'To-Do Website', 'description': 'To-Do Tasks Website', 'author': 'Daniel Reis', 'depends': ['todo_kanban']}
We are building on top of the todo_kanban
addon module, so that we have all the features available added to the To-do Tasks model throughout the book.
Notice that right now we are not depending on the website
addon module. While website
provides a useful framework to build full featured websites, the basic web capabilities are built into the core framework. Let's explore them.
To provide our first web page, we will add a controller object. We can begin by having its file imported with the module:
First add a todo_website/__init__.py
file with following line:
from . import controllers
And then add a todo_website/controllers/__init__.py
file with following line:
from . import main
Now add the actual file for the controller, todo_website/controllers/main.py
, with the following code:
# -*- coding: utf-8 -*- from odoo import http class Todo(http.Controller): @http.route('/helloworld', auth='public') def hello_world(self): return('<h1>Hello World!</h1>')
The odoo.http
module provides the Odoo web-related features. Our controllers, responsible for page rendering, should be objects inheriting from the odoo.http.Controller
class. The actual name used for the class is not important; here we chose to use Main
.
Inside the controller class we have methods, that match routes, does some processing, and then returns a result; the page to be shown to the user.
The odoo.http.route
decorator is what binds a method to a URL route. Our example uses the /hello
route. Navigate to http://localhost:8069/hello
and you will be greeted with a Hello World message. In this example the processing performed by the method is quite trivial: it just returns a text string with the HTML markup for the Hello World message.
You probably noticed that we added the auth='public'
argument to the route. This is needed for the page to be available to non-authenticated users. If we remove it, only authenticated users can see the page. If no session is active, the login screen will be shown instead.
Using Python strings to build HTML will get boring very fast. QWeb templates do a much better job at that. So let's improve our Hello World web page to use a template instead.
QWeb templates are added through XML data files, and technically they are a type of view, alongside form or tree views. They are actually stored in the same model, ir.ui.view
.
As usual, data files to be loaded must be declared in the manifest file, so edit the todo_website/__manifest__.py
file to add the key:
'data': ['views/todo_templates.xml'],
And then add the actual data file, views/todo_web.xml
, with the following content:
<odoo> <template id="hello" name="Hello Template"> <h1>Hello World !</h1> </template> </odoo>
Now we need to have our controller method use this template:
from odoo.http import request # ... @http.route('/hello', auth='public') def hello(self, **kwargs): return request.render('todo_website.hello')
Template rendering is provided by request
, through its render()
function.
Notice that we added **kwargs
to the method arguments. With this if any additional parameters provided by the HTTP request, such as query string or POST
parameters, can be captured by the kwargs
dictionary. This makes our method more robust, since providing unexpected parameters will not cause it to error.
Extensibility is something we expect in all features of Odoo, and the web features are no exception. And indeed we can extend existing controllers and templates. As an example, we will extend our Hello World web page so that it takes a parameter with the name to greet: using the URL /hello?name=John
would return a Hello John! greeting.
Extending is usually done from a different addon module, but it works as well inside the same addon. To keep things concise and simple, we'll do it without creating a new addon module.
Let's add a new todo_website/controllers/extend.py
file with the following code:
# -*- coding: utf-8 -*- from odoo import http from odoo.addons.todo_website.controllers.main import Todo class TodoExtended(Todo): @http.route() def hello(self, name=None, **kwargs): response = super(TodoExtended, self).hello() response.qcontext['name'] = name return response
Here we can see what we need to do to extend a controller.
First we use a Python import
to get a reference to the controller class we want to extend. Compared with models, they have a central registry, provided by the env
object, where a reference to any model class can be obtained, without the need to know the module and file implementing them. With controllers we don't have that, and need to know the module and file implementing the controller we want to extend.
Next we need to (re)define the method from the controller being extended. It needs to be decorated with at least the simple @http.route()
for its route to be kept active. Optionally, we can provide parameters to route()
, and then we will be replacing and redefining its routes.
The extended hello()
method now has a name
parameter. The parameters can get their values from segments of the route URL, from query string parameters, or from POST
parameters. In this case, the route has no extractable variable (we'll show that in a moment), and since we are handling GET
requests, not POST
, the value for the name parameter will be extracted from the URL query string. A test URL could be http://localhost:8069/hello?name=John
.
Inside the hello()
method we run the inherited method to get its response, and then get to modify it according to our needs. The common pattern for controller methods is for them to end with a statement to render a template. In our case:
return request.render('todo_website.hello')
This generates a http.Response
object, but the actual rendering is delayed until the end of the dispatching.
This means that the inheriting method can still change the QWeb template and context to use for the rendering. We could change the template modifying response.template
, but we won't need that. We rather want to modify response.qcontext
to add the name
key to the rendering context.
Don't forget to add the new Python file to todo_website/controllers/__init__.py
:
from . import main
from . import extend
Now we need to modify the QWeb template, so that it makes use of this additional piece of information. Add a todo/website/views/todo_extend.xml
:
<odoo>
<template id="hello_extended"
name="Extended Hello World"
inherit_id="todo_website.hello">
<xpath expr="//h1" position="replace">
<h1>
Hello <t t-esc="name or 'Someone'" />!
</h1>
</xpath>
</template>
</odoo>
Web page templates are XML documents, just like the other Odoo view types, and we can use xpath
to locate elements and then manipulate them, just like we could with the other view types. The inherited template is identified in the <template>
element by the inherited_id
attribute.
We ought not forget to declare this additional data file in our addon manifest, todo_website/__manifest__.py
:
'data': [
'views/todo_web.xml',
'views/todo_extend.xml'],
After this, accessing http://localhost:8069/hello?name=John
should show us a Hello John! message.
We can also provide parameters through URL segments. For example, we could get the exact same result from the http://localhost:8069/hello/John
URL using this alternative implementation:
class TodoExtended(Todo):
@http.route(['/hello', '/hello/<name>])
def hello(self, name=None, **kwargs):
response = super(TodoExtended, self).hello()
response.qcontext['name'] = name
return response
As you can see, routes can contain placeholders corresponding to parameters to be extracted, and then passed on to the method. Placeholders can also specify a converter to implement a specific type mapping. For example, <int:user_id>
would extract the user_id
parameter as an integer value.
Converters are a feature provided by the werkzeug
library, used by Odoo, and most of the ones available can be found in werkzeug
library's documentation, at http://werkzeug.pocoo.org/docs/routing/.
Odoo adds a specific and particularly helpful converter: extracting a model record. For example @http.route('/hello/<model("res.users"):user>)
extracts the user parameter as a record object on for the res.users
model.
Let's make this even more interesting, and create our own simple CMS. For this we can have the route expect a template name (a page) in the URL and then just render it. We could then dynamically create web pages and have them served by our CMS.
It turns out that this is quite easy to do:
@http.route('/hellocms/<page>', auth='public') def hello(self, page, **kwargs): return http.request.render(page)
Now, open http://localhost:8069/hellocms/todo_website.hello
in your web browser and you will see our Hello World web page!
In fact, the built-in website provides CMS features including a more robust implementation of the above, at the /page
endpoint route.
Most of the time we want our pages to be integrated into the Odoo website. So for the remainder of this chapter our examples we will be working with the website
addon.