Website Forms: Turning Visitor Input Into Records
Odoo's website builder lets you drop a form on a page that creates a record on submit — a contact request, a job application, a lead. It posts to a built-in generic endpoint that maps fields to a model.
<form action="/website/form/" method="post" data-model_name="crm.lead">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="text" name="contact_name" required="1"/>
<input type="email" name="email_from" required="1"/>
<textarea name="description"></textarea>
<button type="submit">Send</button>
</form>
The data-model_name tells Odoo which model to create; each input's name must match a field on that model. The /website/form/ controller validates and creates the record.
| Concern | Handling |
|---|---|
| CSRF | include the csrf_token — required, blocks forged posts |
| Allowed fields | a model's _website_form_fields whitelist limits what can be set |
| Spam | add a honeypot/captcha for public forms |
Security matters here because the form is public. Odoo only lets website forms write to fields a model explicitly whitelists (via _website_form_fields / the website_form access), so a visitor can't set arbitrary fields like user_id or prices by injecting hidden inputs. Always keep the CSRF token, validate/whitelist server-side, and add anti-spam (captcha) on any public-facing form. For custom target models, you must opt the model and its safe fields into website forms explicitly.
🏋️ Practical Exercise
- Add a form snippet to a page.
- Map form fields to a model.
- Handle the submission.
- Add a success message.
- Add spam protection (concept).
🔥 Challenge Exercise
Build a public contact/lead form on a website page that creates a record in a backend model on submit, with validation and a thank-you message. Explain how the Website Form snippet maps fields to a model and how submissions are stored.
- The built-in
website_formwidget and when to use it - Writing a custom POST controller for website form submissions
- CSRF and SPAM protection on public forms
- Handling file uploads and confirmation redirects
The website_form Widget
For standard forms that create records (leads, job applications, contact messages), the website_form JavaScript widget handles submission, validation, and SPAM protection automatically. Enable it by adding data-model-name to your form tag:
<form id="contact_form"
class="s_website_form"
action="/website_form/"
method="post"
data-model-name="crm.lead"
data-success-page="/contactus-thank-you">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<div class="form-group">
<label for="contact_name">Name</label>
<input type="text" class="form-control"
name="contact_name" required="required"/>
</div>
<div class="form-group">
<label for="email_from">Email</label>
<input type="email" class="form-control"
name="email_from" required="required"/>
</div>
<div class="form-group">
<label for="description">Message</label>
<textarea class="form-control" name="description" rows="5"/>
</div>
<button type="submit" class="btn btn-primary">Send</button>
</form>
The website_form widget posts to /website_form/{model_name} and creates a record using the form field names as model field names. The model must allow website form creation — configure this in Website → Configuration → Form Allowed Models.
Custom POST Controller
For custom logic (multi-step forms, complex validation, multiple model writes), write your own controller:
from odoo import http
from odoo.http import request
class LibraryWebsite(http.Controller):
@http.route('/books/request', type='http',
auth='public', website=True, methods=['GET', 'POST'])
def book_request_form(self, **kw):
if request.httprequest.method == 'POST':
return self._handle_book_request(kw)
books = request.env['library.book'].sudo().search([
('website_published', '=', True)
])
return request.render('my_module.book_request_form', {
'books': books, 'errors': {}, 'values': {},
})
def _handle_book_request(self, values):
errors = {}
name = (values.get('visitor_name') or '').strip()
email = (values.get('visitor_email') or '').strip()
book_id = int(values.get('book_id') or 0)
if not name:
errors['visitor_name'] = 'Name is required.'
if not email or '@' not in email:
errors['visitor_email'] = 'Valid email required.'
if not book_id:
errors['book_id'] = 'Please select a book.'
if errors:
books = request.env['library.book'].sudo().search([
('website_published', '=', True)
])
return request.render('my_module.book_request_form', {
'books': books, 'errors': errors, 'values': values,
})
request.env['library.book.request'].sudo().create({
'visitor_name': name,
'visitor_email': email,
'book_id': book_id,
})
return request.redirect('/books/request/thank-you')
CSRF Protection on Public Forms
All POST forms must include the CSRF token. For public (unauthenticated) pages, the token is still required:
<form method="post" action="/books/request">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
...
</form>
Without the CSRF token, Odoo returns HTTP 403. The token is tied to the visitor's session, which Odoo creates automatically even for anonymous visitors.
SPAM Protection
Public forms are targets for bot spam. Odoo's website_form widget adds a honeypot field automatically. For custom forms, implement your own protection:
- Honeypot field — an invisible input; if filled in, it's a bot:
<!-- Hidden from real users via CSS, filled by bots -->
<input type="text" name="website_url"
style="display:none" tabindex="-1" autocomplete="off"/>
# In controller: reject if honeypot filled
if values.get('website_url'):
return request.redirect('/books/request') # silently ignore bots
File Uploads
For file uploads, add enctype="multipart/form-data" to the form and access files in the controller:
import base64
attachment = request.httprequest.files.get('document')
if attachment and attachment.filename:
data = attachment.read()
# Validate size (max 5 MB)
if len(data) > 5 * 1024 * 1024:
errors['document'] = 'File must be under 5 MB.'
else:
request.env['ir.attachment'].sudo().create({
'name': attachment.filename,
'datas': base64.b64encode(data),
'res_model': 'library.book.request',
'res_id': book_request.id,
})
- Use the
website_formwidget for simple record-creation forms — it handles SPAM and validation automatically - For custom logic, write a GET/POST controller with manual validation and a POST/Redirect/GET pattern
- Always include
csrf_tokenin every POST form - Validate file type and size before creating
ir.attachmentrecords
Interview Questions
- How do you add a form to an Odoo website?
- How are form fields mapped to a model?
- How is a submission stored?
- How do you validate website form input?
- How do you protect a public form from spam?
Related Topics
FAQ
Use website_form when you're creating a single record (lead, contact message, job application) and standard field mapping is sufficient. Use a custom controller when you need multi-model writes, complex validation, file uploads alongside record creation, or conditional logic based on submitted values.
Redirect to a dedicated thank-you URL after successful processing: return request.redirect('/my-form/thank-you'). Create a simple controller and template for that URL. This also prevents duplicate submissions on browser refresh (POST/Redirect/GET pattern).
Yes — the google_recaptcha module integrates reCAPTCHA v3 with the website_form widget. Configure the site key in Website → Configuration → Settings. For custom forms, call request.env['ir.recaptcha']._verify_recaptcha_token(token) in your controller.

