The pages given by the previous examples are not integrated into the Odoo website: we have no page footer, menu, and so on. The Odoo website addon module conveniently provides all these features so that we don't have to worry about them ourselves.

To use it, we should start by installing the website addon module in our work instance, and then add it as a dependency to our module. The __manifest__.py key depends should look like this:

'depends': ['todo_kanban', 'website'], 

To use the website, we also need to modify the controller and the template.

The controller needs an additional website=True argument on the route:

@http.route('/hello', auth='public', website=True) 
def hello(self, **kwargs): 
    return request.render('todo_website.hello') 

And the template needs to be inserted inside the website general layout:

<template id="hello" name="Hello World"> 
  <t t-call="website.layout"> 
    <h1>Hello World!</h1> 
  </t> 
</template> 

With this, the Hello World! example we used before should now be shown inside an Odoo website page.

Now that we went through the basics, let's work on our Todo Task list. We will have a /todo URL showing us a web page with a list of Todo Tasks.

For that, we need a controller method, preparing the data to present, and a QWeb template to present that list to the user.

Edit the todo_website/controllers/main.py file, to add this method:

#class Main(http.Controller): 
   
    @http.route('/todo', auth='user' , website=True) 
    def index(self, **kwargs): 
        TodoTask = request.env['todo.task'] 
        tasks =  TodoTask.search([]) 
        return request.render( 
            'todo_website.index', {'tasks': tasks}) 

The controller retrieves the data to be used and makes it available to the rendered template. In this case the controller requires an authenticated session, since the route has the auth='user' attribute. Even if that is the default value, it's a good practice to explicitly state that a user session is required. 

With this, the Todo Task search() statement will run with the current session user.

The data accessible to public users is very limited, when using that type of route, we often need to use sudo() to elevate access and make the page data available that otherwise would not be accessible.

This can also be a security risk, so be careful on the validation of the input parameters and on the actions made. Also keep the sudo() recordset usage limited to the minimum operations possible.  

The request.render() method expects the identifier of the QWeb template to render, and a dictionary with the context available for the template evaluation.

The QWeb template should be added by a data file, and we can add it to the existing todo_website/views/todo_templates.xml data file:

<template id="index" name="Todo List"> 
  <t t-call="website.layout"> 
    <div id="wrap" class="container"> 
      <h1>Todo Tasks</h1> 
 
      <!-- List of Tasks --> 
      <t t-foreach="tasks" t-as="task"> 
        <div class="row"> 
          <input type="checkbox" disabled="True" 
            t-att-checked=" 'checked' if task.is_done else {}" /> 
          <a t-attf-href="/todo/{{slug(task)}}"> 
            <span t-field="task.name" 
              t-att-class="'todo-app-done' if task.is_done  
                else ''" /> 
          </a> 
        </div> 
      </t> 
 
      <!-- Add a new Task --> 
      <div class="row"> 
        <a href="/todo/add" class="btn btn-primary btn-lg"> 
            Add 
        </a> 
      </div> 
 
    </div> 
  </t> 
</template> 

The preceding code uses the t-foreach directive to render a list of tasks. The t-att directive used on the input checkbox allows us to add, or not, a checked attribute depending on the is_done value.

We have a checkbox input, and want it to be checked if the task is done. In HTML, a checkbox is checked depending on it having or not a checked attribute. For this we use the t-att-NAME directive to dynamically render the checked attribute depending on an expression. In this case, the expression evaluates to None, QWeb will omit the attribute, which is convenient for this case.

When rendering the task name, the t-attf directive is used to dynamically create the URL to open the detail form for each specific task. We used the special function slug() to generate a human-readable URL for each record. The link won't work for now, since we are still to create the corresponding controller.

On each task we also use the t-att directive to set the todo-app-done style only for the tasks that are done.

Finally, we have an Add button to open a page with a form to create a new Todo Task. We will use it to introduce web form handling next.