Forms are a common feature found on websites. We already have all the tools needed to implement one: a QWeb template can provide the HTML for the form, the corresponding submit action can be an URL, processed by a controller that can run all the validation logic, and finally store the data in the proper model.
But for non-trivial forms this can be a demanding task. It's not that simple to perform all the needed validations and provide feedback to the user about what is wrong.
Since this is a common need, a website_form
addon is available to aid us with this. Let's see how to use it.
Looking back at the Add button in the Todo Task list, we can see that it opens the /todo/add
URL. This will present a form to submit a new Todo Task, and the fields available will be the task name, a person (user) responsible for the task, and a file attachment.
We should start by adding the website_form
dependency to our addon module. We can replace website
, since keeping it explicitly would be redundant. On the todo_website/__manifest__.py
edit the depends
keyword to:
'depends': ['todo_kanban', 'website_form'],
Now we will add the page with the form.
We can start by implementing the controller method to support the form rendering, in the todo_website/controllers/main.py
file:
@http.route('/todo/add', website=True) def add(self, **kwargs): users = request.env['res.users'].search([]) return request.render( 'todo_website.add', {'users': users})
This is a simple controller, rendering the todo_website.add
template, and providing it with a list of users, so that it can be used to build a selection box.
Now for the corresponding QWeb template. We can add it into the todo_website/views/todo_web.xml
data file:
<template id="add" name="Add Todo Task"> <t t-call="website.layout"> <t t-set="additional_title">Add Todo</t> <div id="wrap" class="container"> <div class="row"> <section id="forms"> <form method="post" class="s_website_form container-fluid form-horizontal" action="/website_form/" data-model_name="todo.task" data-success_page="/todo" enctype="multipart/form-data" > <!-- Form fields will go here! --> <!-- Submit button --> <div class="form-group"> <div class="col-md-offset-3 col-md-7 col-sm-offset-4 col-sm-8"> <a class="o_website_form_send btn btn-primary btn-lg"> Save </a> <span id="o_website_form_result"></span> </div> </div> </form> </section> </div> <!-- rows --> </div> <!-- container --> </t> <!-- website.layout --> </template>
As expected, we can find the Odoo-specific <t t-call="website.layout">
element, responsible for inserting the template inside the website layout, and the <t t-set="additional_title">
that sets an additional title, expected by the website layout.
For the content, most of what we can see in this template can be found on a typical Bootstrap CSS form. But we also have a few attributes and CSS classes that are specific to the website forms. We marked them in bold in the code, so that it's easier for you to identify them.
The CSS classes are needed for the JavaScript code to be able to correctly perform its form handling logic. And then we have a few specific attributes on the <form>
element:
action
is a standard form attribute, but must have the "/website_form/"
value. The trailing slash is required.data-model_name
identifies the model to write to, and will be passed to the /website_form
controller.data-success_page
is the URL to redirect to after a successful form submission. In this case we will be sent back to the /todo
list.We won't need to provide our own controller method to handle the form submission. The /website_form
route will do that for us. It takes all information it needs from the form, including the specific attributes just described, and then performs essential validations on the input data, and creates a new record on the target model.
For advanced use cases, we can force a custom controller method to be used. For that we should add a data-force_action
attribute to the <form>
element, with the keyword for the target controller to use. For example, data-force_action="todo-custom"
would have the form submission to call the /website_form/todo-custom
URL. We should then provide a controller method attached to that route. However, doing this will be out of our scope here.
We still need to finish our form, adding the fields to get inputs from the user. Inside the <form>
element add:
<!-- Description text field, required --> <div class="form-group form-field"> <div class="col-md-3 col-sm-4 text-right"> <label class="control-label" for="name">To do*</label> </div> <div class="col-md-7 col-sm-8"> <input name="name" type="text" required="True" class="o_website_from_input form-control" /> </div> </div> <!-- Add an attachment field --> <div class="form-group form-field"> <div class="col-md-3 col-sm-4 text-right"> <label class="control-label" for="file_upload"> Attach file </label> </div> <div class="col-md-7 col-sm-8"> <input name="file_upload" type="file" class="o_website_from_input form-control" /> </div> </div>
Here we are adding two fields, a regular text field for the description and a file field, to upload an attachment. All the markup can be found in regular Bootstrap forms, except for the o_website_from_input
class, needed for the website form logic to prepare the data to submit.
The user selection list is not much different except that it needs to use a t-foreach
QWeb directive to render the list of selectable users. We can do this because the controller retrieves that recordset and makes it available to the template under the name users
:
<!-- Select User --> <div class="form-group form-field"> <div class="col-md-3 col-sm-4 text-right"> <label class="control-label" for="user_id"> For Person </label> </div> <div class="col-md-7 col-sm-8"> <select name="user_id" class="o_website_from_input form-control" > <t t-foreach="users" t-as="user"> <option t-att-value="user.id"> <t t-esc="user.name" /> </option> </t> </select> </div> </div>
However, our form still won't work until we do some access security setup.
Since this generic form handling is quite open, and relies on untrusted data sent by the client, for security reasons it needs some server-side set up on what the client is allowed to do. In particular, the model fields that can be written based on form data should be whitelisted.
To add fields to this whitelist, a helper function is provided and we can use it from an XML data file. We should create the todo_website/data/config_data.xml
file with:
<?xml version="1.0" encoding="utf-8"?> <odoo> <data> <record id="todo_app.model_todo_task" model="ir.model"> <field name="website_form_access">True</field> </record> <function model="ir.model.fields" name="formbuilder_whitelist"> <value>todo.task</value> <value eval="['name', 'user_id', 'date_deadline']"/> </function> </data> </odoo>
For a model to be able to be used by forms, we must do two things: enable a flag on the model, and whitelist the field that can be used. These are the two actions being done in the preceding data file.
Don't forget that, for our addon module to know about this data file, it needs to be added to the data
key of the manifest file.
It would also be nice for our Todo page to be available from the website menu. Let's add that using this same data file. Add another <data>
element like this:
<data noupdate="1"> <record id="menu_todo" model="website.menu"> <field name="name">Todo</field> <field name="url">/todo</field> <field name="parent_id" ref="website.main_menu"/> <field name="sequence" type="int">50</field> </record> </data>
As you can see, to add a website menu item we just need to create a record in the website.menu
model, with a name, URL, and the identifier of the parent menu item. The top level of this menu has as parent; the website.main_menu
item.
Website forms allow us to plug in our own validations and computations to the form processing. This is done by implementing a website_form_input_filter()
method with the logic on the target model. It accepts a values
dictionary, validates and makes changes to it, and then returns the possibly modified values
dictionary.
We will use it to implement two features: remove any leading and trailing spaces from the task title, and enforce that the task title must be at least three characters long.
Add the todo_website/models/todo_task.py
file containing the following code:
# -*- coding: utf-8 -*- from odoo import api, models from odoo.exceptions import ValidationError class TodoTask(models.Model): _inherit = 'todo.task' @api.model def website_form_input_filter(self, request, values): if 'name' in values: values['name'] = values['name'].strip() if len(values.['name']) < 3: raise ValidationError( 'Text must be at least 3 characters long') return values
The website_form_input_filter
method actually expects two parameters: the request
object and the values
dictionary. Errors preventing form submission should raise a ValidationError
exception.
Most of the time this extension point for forms should allow us to avoid custom form submission handlers.
As usual, we must make this new file Python imported, by adding from .import models
in the todo_website/__init__.py
file, and adding the todo_website/models/__init__.py
file with a from . import todo_task
line.