Files
odoo_source/addons/website_form/controllers/main.py
T
Adrien Widart 5a499ecf11 [FIX] website_form: cancel records creation if error
When running an automated action on record creation, if the action
raises a UserError/ValidationError, the record will still be created.

To reproduce the error:
(Need website_crm,base_automation. Enable debug mode)
1. Create an Automated Action
    - Model: Lead/Opportunity
    - Trigger: On Creation & Update
    - Trigger Fields: Phone (crm.lead)
    - Action To Do: Execute Python Code
    - Python Code:
    ```python
    if record.phone:
        raise UserError("Lorem ipsum")
    ```
2. On website, add a website form WF
    - Action: "Create an Opportunity"
3. Complete and submit WF including a phone number

Error: An error message is displayed ("Lorem ispum"). However, the lead
has been created and is present in CRM Pipeline.

An on-creation automated action is executed once the record is executed,
but nothing will cancel this records creation if the action raises a
UserError/ValidationError.

OPW-2423948

closes odoo/odoo#69911

X-original-commit: 8d452ae16b7e6c3077d9eae135852bada43cec93
Signed-off-by: Raphael Collet (rco) <rco@openerp.com>
Signed-off-by: Adrien Widart <adwid@users.noreply.github.com>
2021-04-27 11:32:26 +00:00

275 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import json
import pytz
from datetime import datetime
from psycopg2 import IntegrityError
from werkzeug.exceptions import BadRequest
from odoo import http, SUPERUSER_ID, _
from odoo.http import request
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools.translate import _
from odoo.exceptions import ValidationError, UserError
from odoo.addons.base.models.ir_qweb_fields import nl2br
class WebsiteForm(http.Controller):
@http.route('/website_form/', type='http', auth="public", methods=['POST'], multilang=False)
def website_form_empty(self, **kwargs):
# This is a workaround to don't add language prefix to <form action="/website_form/" ...>
return ""
# Check and insert values from the form on the model <model>
@http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'], website=True, csrf=False)
def website_form(self, model_name, **kwargs):
# Partial CSRF check, only performed when session is authenticated, as there
# is no real risk for unauthenticated sessions here. It's a common case for
# embedded forms now: SameSite policy rejects the cookies, so the session
# is lost, and the CSRF check fails, breaking the post for no good reason.
csrf_token = request.params.pop('csrf_token', None)
if request.session.uid and not request.validate_csrf(csrf_token):
raise BadRequest('Session expired (invalid CSRF token)')
try:
# The except clause below should not let what has been done inside
# here be committed. It should not either roll back everything in
# this controller method. Instead, we use a savepoint to roll back
# what has been done inside the try clause.
with request.env.cr.savepoint():
if request.env['ir.http']._verify_request_recaptcha_token('website_form'):
return self._handle_website_form(model_name, **kwargs)
error = _("Suspicious activity detected by Google reCaptcha.")
except (ValidationError, UserError) as e:
error = e.args[0]
return json.dumps({
'error': error,
})
def _handle_website_form(self, model_name, **kwargs):
model_record = request.env['ir.model'].sudo().search([('model', '=', model_name), ('website_form_access', '=', True)])
if not model_record:
return json.dumps({
'error': _("The form's specified model does not exist")
})
try:
data = self.extract_data(model_record, request.params)
# If we encounter an issue while extracting data
except ValidationError as e:
# I couldn't find a cleaner way to pass data to an exception
return json.dumps({'error_fields' : e.args[0]})
try:
id_record = self.insert_record(request, model_record, data['record'], data['custom'], data.get('meta'))
if id_record:
self.insert_attachment(model_record, id_record, data['attachments'])
# in case of an email, we want to send it immediately instead of waiting
# for the email queue to process
if model_name == 'mail.mail':
request.env[model_name].sudo().browse(id_record).send()
# Some fields have additional SQL constraints that we can't check generically
# Ex: crm.lead.probability which is a float between 0 and 1
# TODO: How to get the name of the erroneous field ?
except IntegrityError:
return json.dumps(False)
request.session['form_builder_model_model'] = model_record.model
request.session['form_builder_model'] = model_record.name
request.session['form_builder_id'] = id_record
return json.dumps({'id': id_record})
# Constants string to make metadata readable on a text field
_meta_label = "%s\n________\n\n" % _("Metadata") # Title for meta data
# Dict of dynamically called filters following type of field to be fault tolerent
def identity(self, field_label, field_input):
return field_input
def integer(self, field_label, field_input):
return int(field_input)
def floating(self, field_label, field_input):
return float(field_input)
def boolean(self, field_label, field_input):
return bool(field_input)
def binary(self, field_label, field_input):
return base64.b64encode(field_input.read())
def one2many(self, field_label, field_input):
return [int(i) for i in field_input.split(',')]
def many2many(self, field_label, field_input, *args):
return [(args[0] if args else (6,0)) + (self.one2many(field_label, field_input),)]
_input_filters = {
'char': identity,
'text': identity,
'html': identity,
'date': identity,
'datetime': identity,
'many2one': integer,
'one2many': one2many,
'many2many':many2many,
'selection': identity,
'boolean': boolean,
'integer': integer,
'float': floating,
'binary': binary,
'monetary': floating,
}
# Extract all data sent by the form and sort its on several properties
def extract_data(self, model, values):
dest_model = request.env[model.sudo().model]
data = {
'record': {}, # Values to create record
'attachments': [], # Attached files
'custom': '', # Custom fields values
'meta': '', # Add metadata if enabled
}
authorized_fields = model.sudo()._get_form_writable_fields()
error_fields = []
custom_fields = []
for field_name, field_value in values.items():
# If the value of the field if a file
if hasattr(field_value, 'filename'):
# Undo file upload field name indexing
field_name = field_name.split('[', 1)[0]
# If it's an actual binary field, convert the input file
# If it's not, we'll use attachments instead
if field_name in authorized_fields and authorized_fields[field_name]['type'] == 'binary':
data['record'][field_name] = base64.b64encode(field_value.read())
field_value.stream.seek(0) # do not consume value forever
if authorized_fields[field_name]['manual'] and field_name + "_filename" in dest_model:
data['record'][field_name + "_filename"] = field_value.filename
else:
field_value.field_name = field_name
data['attachments'].append(field_value)
# If it's a known field
elif field_name in authorized_fields:
try:
input_filter = self._input_filters[authorized_fields[field_name]['type']]
data['record'][field_name] = input_filter(self, field_name, field_value)
except ValueError:
error_fields.append(field_name)
# If it's a custom field
elif field_name != 'context':
custom_fields.append((field_name, field_value))
data['custom'] = "\n".join([u"%s : %s" % v for v in custom_fields])
# Add metadata if enabled # ICP for retrocompatibility
if request.env['ir.config_parameter'].sudo().get_param('website_form_enable_metadata'):
environ = request.httprequest.headers.environ
data['meta'] += "%s : %s\n%s : %s\n%s : %s\n%s : %s\n" % (
"IP" , environ.get("REMOTE_ADDR"),
"USER_AGENT" , environ.get("HTTP_USER_AGENT"),
"ACCEPT_LANGUAGE" , environ.get("HTTP_ACCEPT_LANGUAGE"),
"REFERER" , environ.get("HTTP_REFERER")
)
# This function can be defined on any model to provide
# a model-specific filtering of the record values
# Example:
# def website_form_input_filter(self, values):
# values['name'] = '%s\'s Application' % values['partner_name']
# return values
if hasattr(dest_model, "website_form_input_filter"):
data['record'] = dest_model.website_form_input_filter(request, data['record'])
missing_required_fields = [label for label, field in authorized_fields.items() if field['required'] and not label in data['record']]
if any(error_fields):
raise ValidationError(error_fields + missing_required_fields)
return data
def insert_record(self, request, model, values, custom, meta=None):
model_name = model.sudo().model
if model_name == 'mail.mail':
values.update({'reply_to': values.get('email_from')})
record = request.env[model_name].with_user(SUPERUSER_ID).with_context(mail_create_nosubscribe=True).create(values)
if custom or meta:
_custom_label = "%s\n___________\n\n" % _("Other Information:") # Title for custom fields
if model_name == 'mail.mail':
_custom_label = "%s\n___________\n\n" % _("This message has been posted on your website!")
default_field = model.website_form_default_field_id
default_field_data = values.get(default_field.name, '')
custom_content = (default_field_data + "\n\n" if default_field_data else '') \
+ (_custom_label + custom + "\n\n" if custom else '') \
+ (self._meta_label + meta if meta else '')
# If there is a default field configured for this model, use it.
# If there isn't, put the custom data in a message instead
if default_field.name:
if default_field.ttype == 'html' or model_name == 'mail.mail':
custom_content = nl2br(custom_content)
record.update({default_field.name: custom_content})
else:
values = {
'body': nl2br(custom_content),
'model': model_name,
'message_type': 'comment',
'res_id': record.id,
}
mail_id = request.env['mail.message'].with_user(SUPERUSER_ID).create(values)
return record.id
# Link all files attached on the form
def insert_attachment(self, model, id_record, files):
orphan_attachment_ids = []
model_name = model.sudo().model
record = model.env[model_name].browse(id_record)
authorized_fields = model.sudo()._get_form_writable_fields()
for file in files:
custom_field = file.field_name not in authorized_fields
attachment_value = {
'name': file.filename,
'datas': base64.encodebytes(file.read()),
'res_model': model_name,
'res_id': record.id,
}
attachment_id = request.env['ir.attachment'].sudo().create(attachment_value)
if attachment_id and not custom_field:
record.sudo()[file.field_name] = [(4, attachment_id.id)]
else:
orphan_attachment_ids.append(attachment_id.id)
if model_name != 'mail.mail':
# If some attachments didn't match a field on the model,
# we create a mail.message to link them to the record
if orphan_attachment_ids:
values = {
'body': _('<p>Attached files : </p>'),
'model': model_name,
'message_type': 'comment',
'res_id': id_record,
'attachment_ids': [(6, 0, orphan_attachment_ids)],
'subtype_id': request.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment'),
}
mail_id = request.env['mail.message'].with_user(SUPERUSER_ID).create(values)
else:
# If the model is mail.mail then we have no other choice but to
# attach the custom binary field files on the attachment_ids field.
for attachment_id_id in orphan_attachment_ids:
record.attachment_ids = [(4, attachment_id_id)]