summaryrefslogtreecommitdiff
path: root/addons/website_form
diff options
context:
space:
mode:
authorJeremy Kersten <jke@odoo.com>2019-04-08 07:07:52 +0000
committerJeremy Kersten <jke@odoo.com>2019-04-08 10:07:49 +0000
commit82ea590264f0cfd904ad21dbbd2202ec2f48be57 (patch)
treeb3a07c168d3f5374901e24a891d092876ef6cf5b /addons/website_form
parent48b373f8e40045fc6f307d6758c4a90256419796 (diff)
[MRG] website_form[_editor]: merge modules together
closes odoo/odoo#32491 Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
Diffstat (limited to 'addons/website_form')
-rw-r--r--addons/website_form/__manifest__.py14
-rw-r--r--addons/website_form/data/mail_mail_data.xml19
-rw-r--r--addons/website_form/static/description/icon.pngbin0 -> 5565 bytes
-rw-r--r--addons/website_form/static/description/icon.svg1
-rw-r--r--addons/website_form/static/src/js/website_form_editor.js474
-rw-r--r--addons/website_form/static/src/js/website_form_editor_tour.js634
-rw-r--r--addons/website_form/static/src/xml/website_form_editor.xml257
-rw-r--r--addons/website_form/tests/__init__.py4
-rw-r--r--addons/website_form/tests/test_website_form_editor.py12
-rw-r--r--addons/website_form/views/assets.xml7
-rw-r--r--addons/website_form/views/ir_model_views.xml37
-rw-r--r--addons/website_form/views/snippets.xml88
12 files changed, 1543 insertions, 4 deletions
diff --git a/addons/website_form/__manifest__.py b/addons/website_form/__manifest__.py
index 132f48ade84..0dd557ca5bf 100644
--- a/addons/website_form/__manifest__.py
+++ b/addons/website_form/__manifest__.py
@@ -1,13 +1,19 @@
{
- 'name': 'Generic Form Controller',
- 'category': 'Hidden',
- 'summary': 'Generic controller for web forms',
- 'description': 'Generic controller for web forms',
+ 'name': 'Website Form',
+ 'category': 'Website',
+ 'summary': 'Build custom web forms',
+ 'description': """
+ Customize and create your own web forms.
+ This module adds a new building block in the website builder in order to build new forms from scratch in any website page.
+ """,
'version': '1.0',
'depends': ['website', 'mail'],
'data': [
+ 'data/mail_mail_data.xml',
'views/assets.xml',
+ 'views/ir_model_views.xml',
'views/res_config_view.xml',
+ 'views/snippets.xml',
],
'installable': True,
'auto_install': False,
diff --git a/addons/website_form/data/mail_mail_data.xml b/addons/website_form/data/mail_mail_data.xml
new file mode 100644
index 00000000000..6997b346792
--- /dev/null
+++ b/addons/website_form/data/mail_mail_data.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="mail.model_mail_mail" model="ir.model">
+ <field name="website_form_default_field_id" ref="mail.field_mail_mail__body_html" />
+ <field name="website_form_access">True</field>
+ <field name="website_form_label">Send an E-mail</field>
+ </record>
+ <function model="ir.model.fields" name="formbuilder_whitelist">
+ <value>mail.mail</value>
+ <value eval="[
+ 'subject',
+ 'body_html',
+ 'email_to',
+ 'email_from',
+ 'record_name',
+ 'attachment_ids',
+ ]"/>
+ </function>
+</odoo>
diff --git a/addons/website_form/static/description/icon.png b/addons/website_form/static/description/icon.png
new file mode 100644
index 00000000000..212df7a906a
--- /dev/null
+++ b/addons/website_form/static/description/icon.png
Binary files differ
diff --git a/addons/website_form/static/description/icon.svg b/addons/website_form/static/description/icon.svg
new file mode 100644
index 00000000000..c6dd1249e8e
--- /dev/null
+++ b/addons/website_form/static/description/icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#B06161"/><stop offset="45.785%" stop-color="#984E4E"/><stop offset="100%" stop-color="#7C3838"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M41.118 69H4c-2 0-4-1-4-4V28.375L15 12h41v42L41.118 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#FFF" d="M34 17v-2h2v2h2v2h-2v2h-2v-2h-2v-2h2zM15 42h41v12H15V42zm19.51 8.985a.608.608 0 0 0 .73 0l5.942-4.792c.202-.162.202-.426 0-.589l-.73-.59a.608.608 0 0 0-.731 0l-4.846 3.909-2.262-1.825a.608.608 0 0 0-.731 0l-.73.59c-.202.162-.202.426 0 .589l3.358 2.708zM15 12h41v12H15V12zm0 15h41v12H15V27zm2-13v8h37v-8H17zm0 15v8h37v-8H17zm15 3h6v2h-6v-2z"/></g></g></svg> \ No newline at end of file
diff --git a/addons/website_form/static/src/js/website_form_editor.js b/addons/website_form/static/src/js/website_form_editor.js
new file mode 100644
index 00000000000..9eb80cc854b
--- /dev/null
+++ b/addons/website_form/static/src/js/website_form_editor.js
@@ -0,0 +1,474 @@
+odoo.define('website_form_editor', function (require) {
+ 'use strict';
+
+ /**
+ * @todo this should be entirely refactored
+ */
+
+ var core = require('web.core');
+ var options = require('web_editor.snippets.options');
+ var wUtils = require('website.utils');
+
+ var qweb = core.qweb;
+
+ options.registry['website_form_editor'] = options.Class.extend({
+ xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'],
+
+ start: function () {
+ this.$target.addClass('o_fake_not_editable').attr('contentEditable', false);
+ this.$target.find('label:not(:has(span)), label span').addClass('o_fake_editable').attr('contentEditable', true);
+ },
+
+ // Generic modal code
+ build_modal: function (modal_title, modal_body, on_save) {
+ var self = this;
+
+ // Build the form parameters modal
+ var modal = qweb.render("website_form.modal", {
+ modal_title: modal_title,
+ modal_body: modal_body
+ });
+
+ self.$modal = $(modal);
+ self.$modal.appendTo('body');
+
+ // Process the modal on_save then hide it
+ self.$modal.find("#modal-save").on('click', function (e){
+ if (self.$modal.find("form")[0].checkValidity()) {
+ on_save();
+ self.$modal.modal('hide');
+ } else {
+ _.each(self.$modal.find('input'), function (input) {
+ var $field = $(input).closest('.form-field');
+ $field.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+ if (!input.checkValidity()) {
+ $field.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ }
+ });
+ }
+ });
+
+ // Destroy the modal when it is closed, as we will use many of them
+ self.$modal.on('hidden.bs.modal', function (e) {
+ self.$modal.remove();
+ });
+
+ self.$modal.modal();
+
+ return self.$modal;
+ },
+
+ // Return the fields promise if we already issued a model
+ // fields fetch request, or issue said request.
+ fields: function () {
+ return this.fields_promise || this.fetch_model_fields();
+ },
+
+ fetch_model_fields: function () {
+ return this._rpc({
+ model: "ir.model",
+ method: "get_authorized_fields",
+ args: [this.$target.closest('form').attr('data-model_name')],
+ }).then(function (fields) {
+ // The get_fields function doesn't return the name
+ // in the field dict since it uses it has the key
+ _.each(fields, function (field, field_name) {
+ field.name = field_name;
+ });
+ return fields;
+ });
+ },
+
+ // Choose a model modal
+ website_form_model_modal: function (previewMode, value, $li) {
+ var self = this;
+ this._rpc({
+ model: "ir.model",
+ method: "search_read",
+ args: [
+ [['website_form_access', '=', true]],
+ ['id', 'model', 'name', 'website_form_label']
+ ],
+ }).then(function (models) {
+ // Models selection input
+ var model_selection = qweb.render("website_form.field_many2one", {
+ field: {
+ name: 'model_selection',
+ string: 'Action',
+ records: _.map(models, function (m) {
+ return {
+ id: m.model,
+ display_name: m.website_form_label || m.name,
+ selected: (m.model === self.$target.attr('data-model_name')) ? 1 : null,
+ };
+ }),
+ }
+ });
+
+ // Success page input
+ var success_page = qweb.render("website_form.field_char", {
+ field: {
+ name: 'success_page',
+ string: 'Thank You Page',
+ value: self.$target.attr('data-success_page')
+ }
+ });
+
+ // Form parameters modal
+ self.build_modal(
+ "Form Parameters",
+ model_selection + success_page,
+ function () {
+ var model_name = self.$modal.find("[name='model_selection']").val();
+ var success_page = self.$modal.find("[name='success_page']").val();
+ self.init_form(model_name);
+ self.$target.attr('data-success_page', success_page);
+
+ // Add magic email_to input if model is mail.mail
+ self.$target.find("input.form-field[name='email_to']").remove();
+ if (model_name === 'mail.mail') {
+ var email_to = self.$modal.find("input[name='email_to']").val();
+ self.$target.append("<input class='form-field' type='hidden' name='email_to' value=" + email_to + ">");
+ }
+ }
+ );
+
+ self.$modal.find("label.col-form-label[for='success_page']").css('font-weight', 'normal');
+ wUtils.autocompleteWithPages(self, self.$modal.find("input[name='success_page']"));
+ self.toggle_email_to();
+
+ self.$modal.find("[name='model_selection']").on('change', function () {
+ self.toggle_email_to();
+ });
+
+ // On modal close, if there is no data-model, it means
+ // that the user refused to configure the form on the
+ // first modal, so we remove the snippet.
+ self.$modal.on('hidden.bs.modal', function (e) {
+ if (!self.$target.attr('data-model_name')){
+ self.$target.remove();
+ }
+ });
+ });
+ },
+
+ // Choose a field modal
+ website_form_field_modal: function (previewMode, value, $li) {
+ var self = this;
+
+ this.fields().then(function (fields) {
+ // Make a nice array to render the select input
+ var fields_array = _.map(fields, function (v, k) { return {id: k, name: v.name, display_name: v.string}; });
+ // Filter the fields to remove the ones already in the form
+ var fields_in_form = _.map(self.$target.find('.col-form-label'), function (label) { return label.getAttribute('for'); });
+ var available_fields = _.filter(fields_array, function (field) { return !_.contains(fields_in_form, field.name); });
+ // Render the select input
+ var field_selection = qweb.render("website_form.field_many2one", {
+ field: {
+ name: 'field_selection',
+ string: 'Field',
+ records: _.sortBy(available_fields, 'display_name')
+ }
+ });
+
+ // Form parameters modal
+ self.build_modal(
+ "Field Parameters",
+ field_selection,
+ function () {
+ var selected_field_name = self.$modal.find("[name='field_selection']").val();
+ var selected_field = fields[selected_field_name];
+ self.append_field(selected_field);
+ }
+ );
+ });
+ },
+
+ // Create a custom field
+ website_form_custom_field: function (previewMode, value, $li) {
+ var default_field_name = 'Custom ' + $li.text();
+ this.append_field({
+ name: default_field_name,
+ string: default_field_name,
+ custom: true,
+ type: value,
+ // Default values for x2many fields
+ records: [
+ {
+ id: 'Option 1',
+ display_name: 'Option 1'
+ },
+ {
+ id: 'Option 2',
+ display_name: 'Option 2'
+ },
+ {
+ id: 'Option 3',
+ display_name: 'Option 3'
+ }
+ ],
+ // Default values for selection fields
+ selection: [
+ [
+ 'Option 1',
+ 'Option 1'
+ ],
+ [
+ 'Option 2',
+ 'Option 2'
+ ],
+ [
+ 'Option 3',
+ 'Option 3'
+ ],
+ ]
+ });
+ },
+
+ // Re-render the field and replace the current one
+ // website_form_editor_field_reset: function(previewMode, value, $li) {
+ // var self = this;
+ // var target_field_name = this.$target.find('.col-form-label').attr('for');
+ // this.fields().then(function(fields){
+ // self.render_field(fields[target_field_name]).done(function(field){
+ // self.$target.replaceWith(field);
+ // })
+ // });
+ // },
+
+ toggle_email_to: function () {
+ // Display or remove the magic email_to field for model mail.mail
+ var selected_model_name = this.$modal.find("[name='model_selection']").val();
+ var $current = this.$modal.find(".form-field:has(input[name='email_to'])");
+ if (selected_model_name !== 'mail.mail') {
+ $current.remove();
+ } else if (!$current.length) { // only add field if it's not already here
+ this.$modal.find("form").append(
+ // Email To input
+ $(qweb.render("website_form.field_char", {
+ field: {
+ name: 'email_to',
+ string: 'Recipient email',
+ required: true,
+ value: this.$target.find("input[name='email_to']").val()
+ }
+ }))
+ );
+ }
+ },
+
+ append_field: function (field) {
+ var self = this;
+ this.render_field(field).then(function (field){
+ self.$target.find(".form-group:has('.o_website_form_send')").before(field);
+ });
+ },
+
+ render_field: function (field) {
+ // Convert the required boolean to a value directly usable
+ // in qweb js to avoid duplicating this in the templates
+ field.required = field.required ? 1 : null;
+
+ // Fetch possible values for relation fields
+ var fieldRelationProm;
+ if (field.relation && field.relation !== 'ir.attachment') {
+ fieldRelationProm = this._rpc({
+ model: field.relation,
+ method: 'search_read',
+ args: [
+ field.domain || [],
+ ['display_name']
+ ],
+ }).then(function (records) {
+ field.records = records;
+ });
+ }
+
+ return Promise.resolve(fieldRelationProm).then(function () {
+ var $content = $(qweb.render("website_form.field_" + field.type, {field: field}));
+ $content.find('label:not(:has(span)), label span').addClass('o_fake_editable').attr('contentEditable', true);
+ return $content;
+ });
+ },
+
+ onBuilt: function () {
+ // Open the parameters modal on snippet drop
+ this.website_form_model_modal('click', null, null);
+ },
+
+ init_form: function (model_name) {
+ var self = this;
+ if (model_name !== this.$target.attr('data-model_name')) {
+ // Reset the form
+ this.$target.attr('data-model_name', model_name);
+ this.$target.find(".form-field:not(:has('.o_website_form_send'))").remove();
+
+ // Force fetch the fields of the new model
+ // and render all model required fields
+ this.fetch_model_fields().then(function (fields) {
+ _.each(fields, function (field, field_name){
+ if (field.required) {
+ self.append_field(field);
+ }
+ });
+ });
+ }
+ },
+
+ cleanForSave: function () {
+ var model = this.$target.data('model_name');
+ // because apparently this can be called on the wrong widget and
+ // we may not have a model, or fields...
+ if (model) {
+ // we may be re-whitelisting already whitelisted fields. Doesn't
+ // really matter.
+ var fields = this.$target.find('input.form-field[name=email_to], .form-field:not(.o_website_form_custom) :input').map(function (_, node) {
+ return node.getAttribute('name');
+ }).get();
+ if (fields.length) {
+ // ideally we'd only do this if saving the form
+ // succeeds... but no idea how to do that
+ this._rpc({
+ model: 'ir.model.fields',
+ method: 'formbuilder_whitelist',
+ args: [model, _.uniq(fields)],
+ });
+ }
+ }
+
+ // Prevent saving of the error colors // TODO: would be better on Edit
+ this.$target.find('.o_has_error').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+
+ // Prevent saving of the status message // TODO: would be better on Edit
+ this.$target.find('#o_website_form_result').empty();
+
+ // Update values of custom inputs to mirror their labels
+ var custom_inputs = this.$target.find('.o_website_form_custom .o_website_form_input');
+ _.each(custom_inputs, function (input, index) {
+ // Change the custom field name according to their label
+ var field_label = $(input).closest('.form-field').find('label:first');
+ input.name = field_label.text().trim();
+ field_label.attr('for', input.name);
+
+ // Change the custom radio or checkboxes values according to their label
+ if (input.type === 'radio' || input.type === 'checkbox') {
+ var checkbox_label = $(input).closest('label').text().trim();
+ if (checkbox_label) {
+ input.value = checkbox_label;
+ }
+ }
+ });
+ }
+ });
+
+ // Generic custom field options
+ options.registry['website_form_editor_field'] = options.Class.extend({
+ xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'],
+
+ // Option to toggle inputs required attribute
+ website_form_field_require: function (previewMode, value, $li) {
+ this.$target.find('.o_website_form_input').each(function (index, input) {
+ input.required = !input.required;
+ });
+ }
+ });
+
+ // Dirty hack to transform select fields into an editable construct
+ options.registry['website_form_editor_field_select'] = options.Class.extend({
+ xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'],
+
+ start: function () {
+ if (!this.$target.find('#editable_select').length) {
+ var self = this;
+ var select = this.$target.find('select');
+ select.hide();
+ this.editable_select = $('<div id="editable_select" class="form-control o_website_form_input"/>');
+ _.each(select.children(), function (option) {
+ self.editable_select.append(
+ $('<div id="' + $(option).attr('value') + '" class="o_website_form_select_item">' + $(option).text().trim() + '</div>')
+ );
+ });
+ select.after(this.editable_select);
+ }
+ },
+
+ cleanForSave: function () {
+ if (this.$target.find('#editable_select').length) {
+ var self = this;
+ // Reconstruct the field from the select tag
+ var select = this.$target.find('select');
+ var field = {
+ name: select.attr('name'),
+ string: this.$target.find('.col-form-label').text().trim(),
+ required: self.$target.hasClass('o_website_form_required'),
+ custom: self.$target.hasClass('o_website_form_custom'),
+ };
+
+ // Build the new records list from the editable select field
+ var records = [];
+ var editable_options = this.$target.find('#editable_select .o_website_form_select_item');
+ _.each(editable_options, function (option) {
+ records.push({
+ id: self.$target.hasClass('o_website_form_custom') ? $(option).text().trim() : $(option).attr('id'),
+ display_name: $(option).text().trim()
+ });
+ });
+ field.records = records;
+
+ // Replace this field by the new one
+ var $new_select = $(qweb.render("website_form.field_many2one", {field: field}));
+ // Reapply the custom style classes
+ if (this.$target.hasClass('o_website_form_required_custom')) {
+ $new_select.addClass('o_website_form_required_custom');
+ }
+ if (this.$target.hasClass('o_website_form_field_hidden')) {
+ $new_select.addClass('o_website_form_field_hidden');
+ }
+ this.$target.replaceWith($new_select);
+ }
+ }
+ });
+
+ // Superclass for options that need to disable a button from the snippet overlay
+ var disable_overlay_button_option = options.Class.extend({
+ xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'],
+
+ // Disable a button of the snippet overlay
+ disable_button: function (button_name, message) {
+ // TODO refactor in master
+ var className = 'oe_snippet_' + button_name;
+ this.$overlay.on('click', '.' + className, this.prevent_button);
+ var $button = this.$overlay.find('.' + className);
+ $button.attr('title', message).tooltip({delay: 0});
+ $button.removeClass(className); // Disable the functionnality
+ },
+
+ prevent_button: function (event) {
+ // Snippet options bind their functions before the editor, so we
+ // can't cleanly unbind the editor onRemove function from here
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ });
+
+ // Disable duplicate button for model fields
+ options.registry['website_form_editor_field_model'] = disable_overlay_button_option.extend({
+ start: function () {
+ this.disable_button('clone', 'You can\'t duplicate a model field.');
+ }
+ });
+
+ // Disable delete button for model required fields
+ options.registry['website_form_editor_field_required'] = disable_overlay_button_option.extend({
+ start: function () {
+ this.disable_button('remove', 'You can\'t remove a field that is required by the model itself.');
+ }
+ });
+
+ // Disable duplicate button for non-custom checkboxes and radio buttons
+ options.registry['website_form_editor_field_x2many'] =disable_overlay_button_option.extend({
+ start: function () {
+ this.disable_button('clone', 'You can\'t duplicate an item which refers to an actual record.');
+ }
+ });
+});
diff --git a/addons/website_form/static/src/js/website_form_editor_tour.js b/addons/website_form/static/src/js/website_form_editor_tour.js
new file mode 100644
index 00000000000..42c85592e41
--- /dev/null
+++ b/addons/website_form/static/src/js/website_form_editor_tour.js
@@ -0,0 +1,634 @@
+odoo.define('website_form_editor.tour', function(require) {
+ 'use strict';
+
+ var rpc = require('web.rpc');
+ var tour = require("web_tour.tour");
+
+ tour.register("website_form_editor_tour", {
+ test: true,
+ }, [
+ // Drop a form builder snippet and configure it
+ {
+ content: "Enter edit mode",
+ trigger: "a[data-action=edit]"
+ },
+ {
+ content: "Drop the form snippet",
+ trigger: "#oe_snippets .oe_snippet:has(.s_website_form) .oe_snippet_thumbnail",
+ run: "drag_and_drop #wrap",
+ },
+ {
+ content: "Check dropped snippet",
+ trigger: "body:has(form[action*='/website_form/'])",
+ },
+ {
+ content: "Check if the snippet is dropped and if the modal is opened",
+ trigger: "body:has(form[action*='/website_form/'])" +
+ ":has(.modal-body:has(select[name='model_selection'])" +
+ ":has(input[name='success_page']))",
+ run: function () {},
+ in_modal: false,
+ },
+ {
+ content: "Change the action to create issues",
+ trigger: ".modal-body select",
+ run: "text project.issue"
+ },
+ {
+ content: "Change the action to Send an E-mail",
+ trigger: ".modal-body select",
+ run: "text mail.mail"
+ },
+ {
+ content: "Complete Recipient E-mail",
+ extra_trigger: ".modal-body input[name='email_to']",
+ trigger: ".modal-body input[name='email_to']",
+ run: "text test@test.test"
+ },
+ {
+ content: "Click on Save",
+ trigger: ".modal-footer #modal-save"
+ },
+ // Add the subject field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Add a model field",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_modal]"
+ },
+ {
+ content: "Select the subject field",
+ trigger: "select[name='field_selection']",
+ run: "text subject"
+ },
+ {
+ content: "Click on Save",
+ trigger: ".modal-footer #modal-save"
+ },
+
+ // Customize subject field
+ {
+ content: "Change the label",
+ trigger: ".col-form-label[for='subject']",
+ run: "text Name"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Required",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_require]"
+ },
+ {
+ content: "Check the resulting field",
+ trigger: ".form-field.o_website_form_required_custom" +
+ ":has(input[type=text][name=subject][required])" +
+ ":has(label:contains('Name'))",
+ run: function () {},
+ },
+
+ // Add record_name field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Add a model field",
+ trigger: ".oe_overlay_options:visible .dropdown-item[data-website_form_field_modal]"
+ },
+ {
+ content: "Select the record_name field",
+ trigger: "select[name='field_selection']",
+ run: "text record_name"
+ },
+ {
+ content: "Click on Save",
+ trigger: ".modal-footer #modal-save"
+ },
+
+ // Customize record_name field
+ {
+ content: "Change the label",
+ trigger: ".col-form-label[for='record_name']",
+ run: "text Awesome Label"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Hidden",
+ trigger: ".oe_overlay_options .dropdown-item[data-toggle-class='o_website_form_field_hidden']"
+ },
+ {
+ content: "Check the resulting field",
+ trigger: ".form-field.o_website_form_field_hidden" +
+ ":has(input:not([required])[type=text][name=record_name])" +
+ ":has(label:contains('Awesome Label'))",
+ run: function () {},
+ },
+
+
+ // Add body_html field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Add a model field",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_modal]"
+ },
+ {
+ content: "Select the body_html field",
+ trigger: "select[name='field_selection']",
+ run: "text body_html"
+ },
+ {
+ content: "Click on Save",
+ trigger: ".modal-footer #modal-save"
+ },
+
+ // Customize subject field
+ {
+ content: "Change the label",
+ trigger: ".col-form-label[for='body_html']",
+ run: "text Your Message"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Required",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_require]"
+ },
+ {
+ content: "Check the resulting field",
+ trigger: ".form-field.o_website_form_required_custom" +
+ ":has(textarea[name=body_html][required])" +
+ ":has(label:contains('Your Message'))",
+ run: function () {},
+ },
+
+ // Add recipient_ids relational field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Add a model field",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_modal]"
+ },
+ {
+ content: "Select the recipient_ids field",
+ trigger: "select[name='field_selection']",
+ run: "text recipient_ids"
+ },
+ {
+ content: "Click on Save",
+ trigger: ".modal-footer #modal-save"
+ },
+ {
+ content: "Check the resulting field",
+ trigger: ".form-field:has(.col-form-label:contains('To (Partners)'))",
+ run: function () {},
+ },
+
+ // Add a custom multiple checkboxes field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]",
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']",
+ run: function (actions) {
+ actions.auto();
+ // I didn't find any other way to make that submenu element appear
+ $(".oe_options .dropdown-submenu .dropdown-menu").css("display", "block");
+ }
+ },
+ {
+ content: "Click on Multiple Checkboxes in menu Add a custom field",
+ trigger: ".oe_options .dropdown-item:contains('Multiple Checkboxes')"
+ },
+
+ // Customize custom multiple checkboxes field
+ {
+ content: "Change the label",
+ trigger: ".col-form-label[for='Custom Multiple Checkboxes']",
+ extra_trigger: ".s_website_form:not(:has(.col-form-label[for='Custom Multiple Checkboxes']:eq(1)))",
+ run: "text Products"
+ },
+ {
+ content: "Change Option 1 label",
+ trigger: "label:contains('Option 1') span",
+ run: "text Iphone"
+ },
+ {
+ content: "Change Option 2 label",
+ trigger: "label:contains('Option 2') span",
+ run: "text Galaxy S"
+ },
+ {
+ content: "Click on Option 3",
+ trigger: "label:contains('Option 3') span",
+ },
+ {
+ content: "Duplicate Option 3",
+ trigger: ".oe_overlay_options:visible .oe_snippet_clone",
+ },
+ {
+ content: "Change first Option 3 label",
+ trigger: "label:contains('Option 3'):first span",
+ run: "text Xperia"
+ },
+ {
+ content: "Change last Option label",
+ trigger: "label:contains('Option 3') span",
+ run: "text Wiko Stairway"
+ },
+ {
+ content: "Click on field",
+ trigger: ".col-form-label:contains('Products')"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Required",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_require]"
+ },
+ {
+ content: "Check the resulting field",
+ trigger: ".form-field.o_website_form_custom.o_website_form_required_custom" +
+ ":has(.col-form-label:contains('Products'))" +
+ ":has(.checkbox label:contains('Iphone'):has(input[type='checkbox'][required]))" +
+ ":has(.checkbox label:contains('Galaxy S'):has(input[type='checkbox'][required]))" +
+ ":has(.checkbox label:contains('Xperia'):has(input[type='checkbox'][required]))" +
+ ":has(.checkbox label:contains('Wiko Stairway'):has(input[type='checkbox'][required]))",
+ run: function () {},
+ },
+
+ // Add a custom radio field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]",
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Radio Buttons in menu Add a custom field",
+ trigger: ".oe_options .dropdown-item:contains('Radio Buttons')"
+ },
+
+ // Customize Radio Buttons field
+ {
+ content: "Change the label",
+ trigger: ".col-form-label[for='Custom Radio Buttons']",
+ run: "text Service"
+ },
+ {
+ content: "Change Option 1 label",
+ trigger: "label:contains('Option 1') span",
+ run: "text After-sales Service"
+ },
+ {
+ content: "Change Option 2 label",
+ trigger: "label:contains('Option 2') span",
+ run: "text Invoicing Service"
+ },
+ {
+ content: "Click on Option 3",
+ trigger: "label:contains('Option 3') span",
+ },
+ {
+ content: "Duplicate Option 3",
+ trigger: ".oe_overlay_options:visible .oe_snippet_clone",
+ },
+ {
+ content: "Change first Option 3 label",
+ trigger: "label:contains('Option 3'):first span",
+ run: "text Development Service"
+ },
+ {
+ content: "Change last Option label",
+ trigger: "label:contains('Option 3') span",
+ run: "text Management Service"
+ },
+ {
+ content: "Click on field",
+ trigger: ".col-form-label:contains('Service')"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Required",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_require]"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Required",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_require]",
+ extra_trigger: ".form-field.o_website_form_custom.o_website_form_required_custom" +
+ ":has(.col-form-label:contains('Service'))",
+ },
+ {
+ content: "Check the resulting field",
+ trigger: ".form-field.o_website_form_custom:not(.o_website_form_required_custom)" +
+ ":has(.col-form-label:contains('Service'))" +
+ ":has(.radio label:contains('After-sales Service'):has(input[type='radio']:not([required])))" +
+ ":has(.radio label:contains('Invoicing Service'):has(input[type='radio']:not([required])))" +
+ ":has(.radio label:contains('Development Service'):has(input[type='radio']:not([required])))" +
+ ":has(.radio label:contains('Management Service'):has(input[type='radio']:not([required])))",
+ run: function () {},
+ },
+
+
+ // Add a custom selection field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]",
+ onend: function() {
+ // I didn't find any other way to make that submenu element appear
+ $(".oe_options > ul > li:has(ul) > ul").css("display", "block");
+ }
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Selection in menu Add a custom field",
+ trigger: ".oe_options .dropdown-item:contains('Selection')"
+ },
+
+ // Customize custom selection field
+ {
+ content: "Change the label",
+ trigger: ".col-form-label[for='Custom Selection']",
+ run: "text State"
+ },
+ {
+ content: "Change Option 1 Label",
+ trigger: ".o_website_form_select_item:contains('Option 1')",
+ run: "text Germany"
+ },
+ {
+ content: "Change Option 2 Label",
+ trigger: ".o_website_form_select_item:contains('Option 2')",
+ run: "text Belgium"
+ },
+ {
+ content: "Click on Option 3",
+ trigger: ".o_website_form_select_item:contains('Option 3')",
+ },
+ {
+ content: "Duplicate Option 3",
+ trigger: ".oe_overlay_options:visible .oe_snippet_clone",
+ },
+ {
+ content: "Change first Option 3 label",
+ trigger: ".o_website_form_select_item:contains('Option 3'):first",
+ run: "text France"
+ },
+ {
+ content: "Change last Option label",
+ trigger: ".o_website_form_select_item:contains('Option 3')",
+ run: "text Canada"
+ },
+ {
+ content: "Click on Germany Option",
+ trigger: ".o_website_form_select_item:contains('Germany')",
+ },
+ {
+ content: "Remove Germany Option",
+ trigger: ".oe_overlay_options:visible .oe_snippet_remove",
+ },
+ {
+ content: "Click on field",
+ trigger: ".col-form-label:contains('State')"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Required",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_require]"
+ },
+ {
+ content: "Check the resulting snippet",
+ trigger: ".form-field.o_website_form_custom.o_website_form_required_custom" +
+ ":has(label:contains('State'))" +
+ ":has(select[required]:hidden)" +
+ ":has(.o_website_form_select_item:contains('Belgium'))" +
+ ":has(.o_website_form_select_item:contains('France'))" +
+ ":has(.o_website_form_select_item:contains('Canada'))" +
+ ":not(:has(.o_website_form_select_item:contains('Germany')))",
+ run: function () {},
+ },
+
+ // Add attachment_ids field
+ {
+ content: "Click on Form snippet",
+ trigger: ".s_website_form[data-model_name]"
+ },
+ {
+ content: "Click on Customize",
+ trigger: ".oe_overlay_options [title='Customize']"
+ },
+ {
+ content: "Click on Add a model field",
+ trigger: ".oe_overlay_options .dropdown-item[data-website_form_field_modal]"
+ },
+ {
+ content: "Select the attachment_ids field",
+ trigger: "select[name='field_selection']",
+ run: "text attachment_ids"
+ },
+ {
+ content: "Click on Save",
+ trigger: ".modal-footer #modal-save"
+ },
+
+ // Customize attachment_ids field
+ {
+ content: "Change the label",
+ trigger: ".col-form-label[for='attachment_ids']",
+ run: "text Invoice Scan"
+ },
+ {
+ content: "Check the resulting field",
+ trigger: ".form-field" +
+ ":has(input[type=file][name=attachment_ids])" +
+ ":has(label:contains('Invoice Scan'))",
+ run: function () {
+ $('body').append('<div id="completlyloaded"></div>');
+ },
+ },
+
+ // Save the page
+ {
+ content: "Save the page",
+ trigger: "button[data-action=save]",
+ },
+ {
+ content: "Wait reloading...",
+ trigger: "html:not(:has(#completlyloaded)) div",
+ }
+ ]);
+
+ tour.register("website_form_editor_tour_submit", {
+ test: true,
+ },[
+ {
+ content: "Try to send empty form",
+ extra_trigger: "form[data-model_name='mail.mail']" +
+ "[data-success_page='']" +
+ ":has(.form-field:has(label:contains('Name')):has(input[type='text'][name='subject'][required]))" +
+ ":has(.form-field:has(label:contains('Awesome Label')):hidden)" +
+ ":has(.form-field:has(label:contains('Your Message')):has(textarea[name='body_html'][required]))" +
+ ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Iphone'][required]))" +
+ ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Galaxy S'][required]))" +
+ ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Xperia'][required]))" +
+ ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Wiko Stairway'][required]))" +
+ ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='After-sales Service']:not([required])))" +
+ ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Invoicing Service']:not([required])))" +
+ ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Development Service']:not([required])))" +
+ ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Management Service']:not([required])))" +
+ ":has(.form-field:has(label:contains('State')):has(select[name='State'][required]:has(option[value='Belgium'])))" +
+ ":has(.form-field.o_website_form_required_custom:has(label:contains('State')):has(select[name='State'][required]:has(option[value='France'])))" +
+ ":has(.form-field:has(label:contains('State')):has(select[name='State'][required]:has(option[value='Canada'])))" +
+ ":has(.form-field:has(label:contains('Invoice Scan')))" +
+ ":has(input.form-field[name='email_to'][value='test@test.test'])",
+ trigger: ".o_website_form_send"
+ },
+ {
+ content: "Check if required fields were detected and complete the Name field",
+ extra_trigger: "form:has(#o_website_form_result.text-danger)" +
+ ":has(.form-field:has(label:contains('Name')).o_has_error)" +
+ ":has(.form-field:has(label:contains('Your Message')).o_has_error)" +
+ ":has(.form-field:has(label:contains('Products')).o_has_error)" +
+ ":has(.form-field:has(label:contains('Service')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('State')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Invoice Scan')):not(.o_has_error))",
+ trigger: "input[name=subject]",
+ run: "text Jane Smith"
+ },
+ {
+ content: "Update required field status by trying to Send again",
+ trigger: ".o_website_form_send"
+ },
+ {
+ content: "Check if required fields were detected and complete the Message field",
+ extra_trigger: "form:has(#o_website_form_result.text-danger)" +
+ ":has(.form-field:has(label:contains('Name')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Your Message')).o_has_error)" +
+ ":has(.form-field:has(label:contains('Products')).o_has_error)" +
+ ":has(.form-field:has(label:contains('Service')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('State')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Invoice Scan')):not(.o_has_error))",
+ trigger: "textarea[name=body_html]",
+ run: "text A useless message"
+ },
+ {
+ content: "Update required field status by trying to Send again",
+ trigger: ".o_website_form_send"
+ },
+ {
+ content: "Check if required fields was detected and check a product. If this fails, you probably broke the cleanForSave.",
+ extra_trigger: "form:has(#o_website_form_result.text-danger)" +
+ ":has(.form-field:has(label:contains('Name')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Your Message')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Products')).o_has_error)" +
+ ":has(.form-field:has(label:contains('Service')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('State')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Invoice Scan')):not(.o_has_error))",
+ trigger: "input[name=Products][value='Wiko Stairway']"
+ },
+ {
+ content: "Check another product",
+ trigger: "input[name='Products'][value='Xperia']"
+ },
+ {
+ content: "Check a service",
+ trigger: "input[name='Service'][value='Development Service']"
+ },
+ {
+ content: "Send the form",
+ trigger: ".o_website_form_send"
+ },
+ {
+ content: "Check form is submitted without errors",
+ trigger: "form:has(#o_website_form_result.text-success)" +
+ ":has(.form-field:has(label:contains('Name')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Your Message')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Products')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Service')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('State')):not(.o_has_error))" +
+ ":has(.form-field:has(label:contains('Invoice Scan')):not(.o_has_error))"
+ }
+ ]);
+
+ tour.register("website_form_editor_tour_results", {
+ test: true,
+ }, [
+ {
+ content: "Check mail.mail records have been created",
+ trigger: "body",
+ run: function () {
+ var mailDef = rpc.query({
+ model: 'mail.mail',
+ method: 'search_count',
+ args: [[
+ ['email_to', '=', 'test@test.test'],
+ ['body_html', 'like', 'A useless message'],
+ ['body_html', 'like', 'Service : Development Service'],
+ ['body_html', 'like', 'State : Belgium'],
+ ['body_html', 'like', 'Products : Xperia,Wiko Stairway']
+ ]],
+ });
+ var success = function(model, count) {
+ if (count > 0) {
+ $('body').append('<div id="website_form_editor_success_test_tour_'+model+'"></div>');
+ }
+ };
+ mailDef.then(_.bind(success, this, 'mail_mail'));
+ }
+ },
+ {
+ content: "Check mail.mail records have been created",
+ trigger: "#website_form_editor_success_test_tour_mail_mail"
+ }
+ ]);
+
+ return {};
+});
diff --git a/addons/website_form/static/src/xml/website_form_editor.xml b/addons/website_form/static/src/xml/website_form_editor.xml
new file mode 100644
index 00000000000..0165bd6ee99
--- /dev/null
+++ b/addons/website_form/static/src/xml/website_form_editor.xml
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <!-- Generic Modal Layout -->
+ <t t-name="website_form.modal">
+ <div role="dialog" class="modal fade">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <header class="modal-header">
+ <h4 class="modal-title"><t t-esc="modal_title"/></h4>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span>&amp;times;</span>
+ </button>
+ </header>
+ <main class="modal-body">
+ <form role="form">
+ <t t-raw="modal_body"/>
+ </form>
+ </main>
+ <footer class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="btn btn-primary" id="modal-save">Save</button>
+ </footer>
+ </div>
+ </div>
+ </div>
+ </t>
+
+ <!-- Generic Field Layout -->
+ <t t-name="website_form.field">
+ <div t-att-class="'row form-group form-field' + (field.custom ? ' o_website_form_custom' : '') + (field.required ? ' o_website_form_required' : '')">
+ <div class="col-md-4 text-md-right">
+ <label class="col-form-label" t-att-for="field.name">
+ <t t-esc="field.string"/>
+ </label>
+ </div>
+ <div class="col-md-8">
+ <t t-raw="0"/>
+ </div>
+ </div>
+ </t>
+
+ <!-- Char Field -->
+ <t t-name="website_form.field_char">
+ <t t-call="website_form.field">
+ <input
+ type="text"
+ class="form-control o_website_form_input"
+ t-att-name="field.name"
+ t-att-required="field.required"
+ t-att-value="field.value"
+ />
+ </t>
+ </t>
+
+ <!-- Text Field -->
+ <t t-name="website_form.field_text">
+ <t t-call="website_form.field">
+ <textarea
+ class="form-control o_website_form_input"
+ t-att-name="field.name"
+ t-att-required="field.required"
+ />
+ </t>
+ </t>
+
+ <!-- HTML Field -->
+ <t t-name="website_form.field_html">
+ <!--
+ Maybe use web_editor ? Not sure it actually makes
+ sense to have random people editing html in a form...
+ -->
+ <t t-call="website_form.field_text"/>
+ </t>
+
+ <!-- Integer Field -->
+ <t t-name="website_form.field_integer">
+ <t t-call="website_form.field">
+ <input
+ type="number"
+ class="form-control o_website_form_input"
+ t-att-name="field.name"
+ step="1"
+ t-att-required="field.required"
+ />
+ </t>
+ </t>
+
+ <!-- Float Field -->
+ <t t-name="website_form.field_float">
+ <t t-call="website_form.field">
+ <input
+ type="number"
+ class="form-control o_website_form_input"
+ t-att-name="field.name"
+ step="any"
+ t-att-required="field.required"
+ />
+ </t>
+ </t>
+
+ <!-- Date Field -->
+ <t t-name="website_form.field_date">
+ <t t-call="website_form.field">
+ <t t-set="datepickerID" t-value="_.uniqueId('datepicker')"/>
+ <div class="o_website_form_date input-group date" t-att-id="datepickerID" data-target-input="nearest">
+ <input
+ type="text"
+ class="form-control datetimepicker-input o_website_form_input"
+ t-attf-data-target="##{datepickerID}"
+ t-att-name="field.name"
+ t-att-required="field.required"
+ />
+ <div class="input-group-append" t-attf-data-target="##{datepickerID}" data-toggle="datetimepicker">
+ <div class="input-group-text"><i class="fa fa-calendar"></i></div>
+ </div>
+ </div>
+ </t>
+ </t>
+
+ <!-- Datetime Field -->
+ <t t-name="website_form.field_datetime">
+ <t t-call="website_form.field">
+ <t t-set="datetimepickerID" t-value="_.uniqueId('datetimepicker')"/>
+ <div class="o_website_form_datetime input-group date" t-att-id="datetimepickerID" data-target-input="nearest">
+ <input
+ type="text"
+ class="form-control datetimepicker-input o_website_form_input"
+ t-attf-data-target="##{datetimepickerID}"
+ t-att-name="field.name"
+ t-att-required="field.required"
+ />
+ <div class="input-group-append" t-attf-data-target="##{datetimepickerID}" data-toggle="datetimepicker">
+ <div class="input-group-text"><i class="fa fa-calendar"></i></div>
+ </div>
+ </div>
+ </t>
+ </t>
+
+ <!-- Boolean Field -->
+ <t t-name="website_form.field_boolean">
+ <t t-call="website_form.field">
+ <input
+ type="checkbox"
+ value="Yes"
+ class="o_website_form_input"
+ t-att-name="field.name"
+ t-att-required="field.required"
+ />
+ </t>
+ </t>
+
+ <!-- Selection Field -->
+ <t t-name="website_form.field_selection">
+ <t t-call="website_form.field">
+ <div class="o_website_form_flex">
+ <t t-foreach="field.selection" t-as="option">
+ <div class="radio o_website_form_flex_item">
+ <label>
+ <input
+ type="radio"
+ class="o_website_form_input"
+ t-att-name="field.name"
+ t-att-value="option[0]"
+ t-att-required="field.required"
+ />
+ <span>
+ <t t-esc="option[1]"/>
+ </span>
+ </label>
+ </div>
+ </t>
+ </div>
+ </t>
+ </t>
+
+ <!-- Many2One Field -->
+ <t t-name="website_form.field_many2one">
+ <!-- Binary one2many -->
+ <t t-if="field.relation == 'ir.attachment'">
+ <t t-call="website_form.field_binary"/>
+ </t>
+ <!-- Generic one2many -->
+ <t t-if="field.relation != 'ir.attachment'">
+ <t t-call="website_form.field">
+ <select class="form-control o_website_form_input" t-att-name="field.name" t-att-required="field.required">
+ <t t-foreach="field.records" t-as="record">
+ <option t-att-value="record.id" t-att-selected="record.selected">
+ <t t-esc="record.display_name"/>
+ </option>
+ </t>
+ </select>
+ </t>
+ </t>
+ </t>
+
+ <!-- One2Many Field -->
+ <t t-name="website_form.field_one2many">
+ <!-- Binary one2many -->
+ <t t-if="field.relation == 'ir.attachment'">
+ <t t-call="website_form.field_binary">
+ <t t-set="multiple" t-value="1"/>
+ </t>
+ </t>
+ <!-- Generic one2many -->
+ <t t-if="field.relation != 'ir.attachment'">
+ <t t-call="website_form.field">
+ <div class="o_website_form_flex">
+ <t t-if="!field.records">
+ <input
+ class="o_website_form_input"
+ t-att-name="field.name"
+ t-att-value="record.id"
+ t-att-required="field.required"
+ placeholder="No matching record !"
+ />
+ </t>
+ <t t-foreach="field.records" t-as="record">
+ <div class="checkbox o_website_form_flex_item">
+ <label>
+ <input
+ type="checkbox"
+ class="o_website_form_input"
+ t-att-name="field.name"
+ t-att-value="record.id"
+ t-att-required="field.required"
+ />
+ <span>
+ <t t-esc="record.display_name"/>
+ </span>
+ </label>
+ </div>
+ </t>
+ </div>
+ </t>
+ </t>
+ </t>
+
+ <!-- Many2Many Field -->
+ <t t-name="website_form.field_many2many">
+ <t t-call="website_form.field_one2many"/>
+ </t>
+
+ <!-- Binary Field -->
+ <t t-name="website_form.field_binary">
+ <t t-call="website_form.field">
+ <input
+ type="file"
+ class="form-control o_website_form_input"
+ t-att-name="field.name"
+ t-att-required="field.required"
+ t-att-multiple="multiple"
+ />
+ </t>
+ </t>
+
+</templates>
diff --git a/addons/website_form/tests/__init__.py b/addons/website_form/tests/__init__.py
new file mode 100644
index 00000000000..b7c97e7f8c5
--- /dev/null
+++ b/addons/website_form/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_website_form_editor \ No newline at end of file
diff --git a/addons/website_form/tests/test_website_form_editor.py b/addons/website_form/tests/test_website_form_editor.py
new file mode 100644
index 00000000000..0bb993cb051
--- /dev/null
+++ b/addons/website_form/tests/test_website_form_editor.py
@@ -0,0 +1,12 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+# -*- coding: utf-8 -*-
+
+import odoo.tests
+
+
+@odoo.tests.tagged('post_install','-at_install')
+class TestWebsiteFormEditor(odoo.tests.HttpCase):
+ def test_tour(self):
+ self.start_tour("/", 'website_form_editor_tour', login="admin")
+ self.start_tour("/", 'website_form_editor_tour_submit')
+ self.start_tour("/", 'website_form_editor_tour_results', login="admin")
diff --git a/addons/website_form/views/assets.xml b/addons/website_form/views/assets.xml
index 2ee98adf76c..cdf869294c1 100644
--- a/addons/website_form/views/assets.xml
+++ b/addons/website_form/views/assets.xml
@@ -6,6 +6,13 @@
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/website_form/static/src/scss/website_form.scss"/>
<script type="text/javascript" src="/website_form/static/src/js/website_form.js"/>
+ <script type="text/javascript" src="/website_form/static/src/js/website_form_editor_tour.js"/>
+ </xpath>
+ </template>
+
+ <template id="assets_editor" name="Website Form Editor Assets Editor" inherit_id="website.assets_editor">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/website_form/static/src/js/website_form_editor.js"/>
</xpath>
</template>
diff --git a/addons/website_form/views/ir_model_views.xml b/addons/website_form/views/ir_model_views.xml
new file mode 100644
index 00000000000..2e56769458f
--- /dev/null
+++ b/addons/website_form/views/ir_model_views.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <record id="ir_model_view" model="ir.ui.view">
+ <field name="name">website_form_editor.ir.model.view.form</field>
+ <field name="model">ir.model</field>
+ <field name="inherit_id" ref="base.view_model_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//notebook" position="inside">
+ <page string="Website Forms">
+ <group>
+ <field name="website_form_access"/>
+ <field name="website_form_label"/>
+ <field name="website_form_default_field_id"/>
+ </group>
+ </page>
+ </xpath>
+
+ <xpath expr="//page[@name='base']/group/group/field[@name='translate']" position="after">
+ <field name="website_form_blacklisted"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="ir_model_fields_view" model="ir.ui.view">
+ <field name="name">website_form_editor.ir.model.fields.view.form</field>
+ <field name="model">ir.model.fields</field>
+ <field name="inherit_id" ref="base.view_model_fields_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='translate']" position="after">
+ <field name="website_form_blacklisted"/>
+ </xpath>
+ </field>
+ </record>
+
+
+</odoo>
diff --git a/addons/website_form/views/snippets.xml b/addons/website_form/views/snippets.xml
new file mode 100644
index 00000000000..b47e02eeda5
--- /dev/null
+++ b/addons/website_form/views/snippets.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="s_website_form" name="Form Builder">
+ <form action="/website_form/" method="post" class="s_website_form container-fluid mt32" enctype="multipart/form-data">
+ <div class="container">
+ <div class="form-group row">
+ <div class="offset-lg-3 offset-md-4 col-md-8 col-lg-7">
+ <button class="btn btn-primary btn-lg o_website_form_send">Send</button>
+ <span id="o_website_form_result"></span>
+ </div>
+ </div>
+ </div>
+ </form>
+ </template>
+
+ <template id="remove_external_snippets" inherit_id="website.external_snippets">
+ <xpath expr="//t[@t-install='website_form']" position="replace"/>
+ </template>
+
+ <template id="register_s_website_form" inherit_id="website.snippets" name="Snippet Form Builder">
+ <xpath expr="//div[@id='snippet_feature']//t[@t-snippet][last()]" position="after">
+ <t t-snippet="website_form.s_website_form" t-thumbnail="/website/static/src/img/s_website_form.png"/>
+ </xpath>
+ </template>
+
+ <template id="snippet_options" inherit_id="website.snippet_options">
+ <xpath expr="//*[@id='so_snippet_addition']" position="attributes">
+ <attribute name="data-selector" add=".s_website_form" separator=","/>
+ </xpath>
+ <xpath expr="//div" position="after">
+ <!-- Form -->
+ <div data-js="website_form_editor" data-selector=".s_website_form">
+ <a href="#" class="dropdown-item" data-website_form_model_modal="" data-no-preview="true">Change Form Parameters</a>
+ <a href="#" class="dropdown-item" data-website_form_field_modal="" data-no-preview="true">Add an existing field</a>
+ <div class="dropdown-submenu">
+ <a href="#" class="dropdown-item">Add a custom field</a>
+ <div class="dropdown-menu" role="menu" data-no-preview="true">
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="char">Text</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="text">Long Text</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="integer">Number</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="float">Decimal Number</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="boolean">Checkbox</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="selection">Radio Buttons</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="many2one">Selection</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="one2many">Multiple Checkboxes</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="date">Date</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="datetime">Date &amp; Time</a>
+ <a href="#" class="dropdown-item" role="menuitem" data-website_form_custom_field="binary">File Upload</a>
+ </div>
+ </div>
+ </div>
+
+ <!-- Field -->
+ <div data-js='website_form_editor' data-selector=".form-field" data-drop-near=".form-field">
+ <a href="#" class="dropdown-item" data-toggle-class="o_website_form_field_hidden" data-no-preview="true">Hidden</a>
+ </div>
+
+ <!-- Add move, duplicate and remove controllers to checkboxes and radio buttons -->
+ <div data-selector=".o_website_form_flex_item" data-drop-near=".o_website_form_flex_item"/>
+
+ <!-- Add move and remove controllers to select items -->
+ <div data-selector=".s_website_form .form-field.o_website_form_custom .o_website_form_select_item" data-drop-near=".o_website_form_select_item"/>
+
+ <!-- Required option for fields that are not required fields of the model -->
+ <div data-js='website_form_editor_field' data-selector=".form-field:not(.o_website_form_required)">
+ <a href="#" class="dropdown-item" data-website_form_field_require="" data-toggle-class="o_website_form_required_custom" data-no-preview="true">Required</a>
+ </div>
+
+ <!-- Remove the duplicate options of model fields -->
+ <div data-js="website_form_editor_field_model" data-selector=".s_website_form .form-field:not(.o_website_form_custom)"/>
+
+ <!-- Remove the delete options of model required fields -->
+ <div data-js="website_form_editor_field_required" data-selector=".s_website_form .o_website_form_required"/>
+
+ <!-- Remove the duplicate options of radio and checkboxes of model fields -->
+ <div data-js="website_form_editor_field_x2many" data-selector=".s_website_form .form-field:not(.o_website_form_custom) .o_website_form_flex_item"/>
+
+ <!-- Transform the select inputs into editable constructs -->
+ <div data-js="website_form_editor_field_select" data-selector=".s_website_form .form-field:has(select)"/>
+
+ <!-- Remove the duplicate option of model select items -->
+ <div data-js="website_form_editor_field_x2many" data-selector=".s_website_form .form-field:not(.o_website_form_custom) .o_website_form_select_item" data-drop-near=".o_website_form_select_item"/>
+
+ <!-- Remove the delete options of the Submit button -->
+ <div data-js="website_form_editor_field_required" data-selector=".s_website_form .o_website_form_send"/>
+ </xpath>
+ </template>
+</odoo>