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.

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.