Browse Source

[10.0][ADD] website_form_builder: Exactly what the title says (#402)

* [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says

* fixup! [ADD] website_form_builder: Exactly what the title says
pull/476/head
Jairo Llopis 1 year ago
parent
commit
5b37e3ed6d
19 changed files with 1830 additions and 0 deletions
  1. +138
    -0
      website_form_builder/README.rst
  2. +0
    -0
      website_form_builder/__init__.py
  3. +26
    -0
      website_form_builder/__manifest__.py
  4. +14
    -0
      website_form_builder/demo/assets.xml
  5. +29
    -0
      website_form_builder/demo/ir_model.xml
  6. +164
    -0
      website_form_builder/i18n/es.po
  7. BIN
      website_form_builder/static/description/icon.png
  8. +103
    -0
      website_form_builder/static/description/icon.svg
  9. +24
    -0
      website_form_builder/static/src/css/website_form_builder.less
  10. +507
    -0
      website_form_builder/static/src/js/snippets.js
  11. +159
    -0
      website_form_builder/static/src/js/tour.js
  12. +162
    -0
      website_form_builder/static/src/js/widgets.js
  13. +214
    -0
      website_form_builder/static/src/xml/snippets.xml
  14. +44
    -0
      website_form_builder/static/src/xml/widgets.xml
  15. +26
    -0
      website_form_builder/templates/assets.xml
  16. +156
    -0
      website_form_builder/templates/snippets.xml
  17. +4
    -0
      website_form_builder/tests/__init__.py
  18. +20
    -0
      website_form_builder/tests/test_ui.py
  19. +40
    -0
      website_form_builder/views/ir_model.xml

+ 138
- 0
website_form_builder/README.rst View File

@@ -0,0 +1,138 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: https://www.gnu.org/licenses/lgpl
:alt: License: LGPL-3

====================
Website Form Builder
====================

This module provides websites the feature of adding custom forms in any page.

Installation
============

Install some other addon that provides ``website_form`` support to
benefit from this one's features. Hints:

* ``website_crm``
* ``website_form_project``
* ``website_hr_recruitment``
* ``website_sale``

Configuration
=============

To configure this module, you need to:

#. Have *Administration / Settings* privileges.
#. Go to *Settings > Activate developer mode*.
#. Go to *Settings > Technical > Database Structure > Models*.
#. Search for the model you want to manage website form access for.
#. When you find it, it will have a *Website Forms* section where you can:

* Allow the model to get forms, by checking *Allowed to use in forms*.
* Give the model forms a better name in *Label for form action*.
* Choose the field where to store custom fields data in *Field for custom
form data*. If you leave this one empty and the model is a mail thread,
a new message will be appended with that custom data.

#. In the *Fields* tab, there's a new column called *Blacklisted in web forms*.
It's a security feature that forbids form submitters to write to those
fields. When you create a new website form, all its model fields are
automatically whitelisted for the sake of improving the UX. If you want to
have higher control, come back here after creating the form and blacklist
any fields you want, although that will only work for custom fields.

Usage
=====

To use this module, you need to:

#. Go to any of your website pages.
#. Edit it.
#. Drag and drop the *Form* snippet into the page.
#. Use the snippet overlay to add, edit and remove fields.
#. If you want to set a hidden field, make sure you set a valid default value
on it, or users may get hidden errors and they might even be unable to send
the form!

.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/186/10.0

Known issues / Roadmap
======================

* These type of fields will not appear, they are forbidden since they make no
sense in this module's context, or a correct implementation would be adding
not much value while adding lots of complexity:

* ``id``
* ``create_uid``
* ``create_date``
* ``write_uid``
* ``write_date``
* ``__last_update``
* Any ``one2many`` fields
* Any ``reference`` fields
* Any ``serialized`` fields
* Any read-only fields

* You should include https://github.com/odoo/odoo/pull/21628 in your
installation to get a better UX when a user has already sent a form and
cannot resend it.

* To edit any ``<label>`` text, you need to click twice. Review the problem
once https://bugzilla.mozilla.org/show_bug.cgi?id=853519 gets fixed.

* You cannot edit base fields blacklisted status manually because
`Odoo forbids that for security
<https://github.com/OCA/website/pull/402#issuecomment-356930433>`_.

* ``website_form`` works in unexpected and undocumented ways. If you plan to
add support in your addon, `this is a good place to start reading
<https://github.com/OCA/website/pull/402#discussion_r157441770>`_.

* If you add a custom file upload field to a form that creates records in
models that have no ``mail.thread`` inheritance, your users will be unable
to send the form.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues
<https://github.com/OCA/website/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.

Credits
=======

Images
------

* https://openclipart.org/detail/281632/form
* https://openclipart.org/detail/224192/simple-grey-small-pencil-icon-white-background

Contributors
------------

* `Tecnativa <https://www.tecnativa.com>`_:
* Jairo Llopis <jairo.llopis@tecnativa.com>

Do not contact contributors directly about support or help with technical issues.

Maintainer
----------

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

This module is maintained by the OCA.

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

To contribute to this module, please visit https://odoo-community.org.

+ 0
- 0
website_form_builder/__init__.py View File


+ 26
- 0
website_form_builder/__manifest__.py View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Tecnativa - Jairo Llopis
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
{
"name": "Website Form Builder",
"summary": "Build customized forms in your website",
"version": "10.0.1.0.0",
"category": "Website",
"website": "https://github.com/OCA/website",
"author": "Tecnativa, Odoo Community Association (OCA)",
"license": "LGPL-3",
"application": False,
"installable": True,
"depends": [
"website_form",
],
"data": [
"templates/assets.xml",
"templates/snippets.xml",
"views/ir_model.xml",
],
"demo": [
"demo/assets.xml",
"demo/ir_model.xml",
],
}

+ 14
- 0
website_form_builder/demo/assets.xml View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->

<odoo>

<template id="demo_assets_editor" inherit_id="website.assets_editor">
<xpath expr=".">
<script type="text/javascript"
src="/website_form_builder/static/src/js/tour.js"/>
</xpath>
</template>

</odoo>

+ 29
- 0
website_form_builder/demo/ir_model.xml View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->

<odoo>
<!-- Enable forms to create countries -->
<record id="base.model_res_country" model="ir.model">
<field name="website_form_access" eval="True"/>
<field name="website_form_label">Country</field>
</record>
<function model="ir.model.fields" name="formbuilder_whitelist">
<value>res.country</value>
<value eval="[
'name',
]"/>
</function>

<!-- Enable forms to create currencies -->
<record id="base.model_res_currency" model="ir.model">
<field name="website_form_access" eval="True"/>
<field name="website_form_label">Currency</field>
</record>
<function model="ir.model.fields" name="formbuilder_whitelist">
<value>res.currency</value>
<value eval="[
'name',
]"/>
</function>
</odoo>

+ 164
- 0
website_form_builder/i18n/es.po View File

@@ -0,0 +1,164 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_form_builder
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-01-19 10:50+0000\n"
"PO-Revision-Date: 2018-01-19 10:52+0000\n"
"Last-Translator: Jairo Llopis <yajo.sk8@gmail.com>\n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.3\n"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/js/snippets.js:317
#, python-format
msgid "%s help block"
msgstr "Bloque de ayuda para %s"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "<i class=\"fa fa-cogs\"/> Change action"
msgstr "<i class=\"fa fa-cogs\"/> Cambiar acción"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "<i class=\"fa fa-eye-slash\"/> Hide field"
msgstr "<i class=\"fa fa-eye-slash\"/> Esconder campo"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "<i class=\"fa fa-hand-spock-o\"/> Add custom field"
msgstr "<i class=\"fa fa-hand-spock-o\"/> Añadir campo personalizado"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "<i class=\"fa fa-plus\"/> Add model fields"
msgstr "<i class=\"fa fa-plus\"/> Añadir campo del modelo"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/js/widgets.js:106
#, python-format
msgid "Add Model Fields"
msgstr "Añadir campo del modelo"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/xml/widgets.xml:15
#, python-format
msgid "Choose the new field for the form"
msgstr "Escoja el nuevo campo para el formulario"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/xml/widgets.xml:30
#, python-format
msgid "Choose this form's action"
msgstr "Escoja la acción de este formulario"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/js/snippets.js:310
#, python-format
msgid "Custom %s field"
msgstr "Campo %s personalizado"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Date"
msgstr "Fecha"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Date and time"
msgstr "Fecha y hora"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Decimal number"
msgstr "Número decimal"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "File upload"
msgstr "Subida de archivos"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/js/widgets.js:75
#, python-format
msgid "Form Settings"
msgstr "Configuración del formulario"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Long text"
msgstr "Texto largo"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Multiple selection"
msgstr "Selección múltiple"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/js/snippets.js:313
#, python-format
msgid "Option %d"
msgstr "Opción %d"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.s_website_form
msgid "Send"
msgstr "Enviar"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Set as required"
msgstr "Hacer obligatorio"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Set default value"
msgstr "Establecer valor por defecto"

#. module: website_form_builder
#. openerp-web
#: code:addons/website_form_builder/static/src/js/widgets.js:35
#, python-format
msgid "Set field's default value"
msgstr "Establecer el valor por defecto para el campo"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Short text"
msgstr "Texto corto"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Single selection"
msgstr "Selección única"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.view_model_form
msgid "Website Forms"
msgstr "Formularios del sitio web"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Whole number"
msgstr "Número entero"

#. module: website_form_builder
#: model:ir.ui.view,arch_db:website_form_builder.snippet_options
msgid "Yes or not"
msgstr "Sí o no"

BIN
website_form_builder/static/description/icon.png View File

Before After
Width: 100  |  Height: 79  |  Size: 1.7KB

+ 103
- 0
website_form_builder/static/description/icon.svg View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
height="79"
width="100"
version="1.1"
id="svg2">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6" />
<style
type="text/css"
id="style3809">
.st0{fill:#414042;}
.st1{fill:none;stroke:#404040;stroke-width:2;stroke-miterlimit:10;}
.st2{fill:none;}
</style>
<g
transform="translate(-13.165603,0.0314306)"
id="g3899">
<g
id="g3837"
transform="matrix(4.3297575,0,0,4.3297575,17.32034,4.8305094)">
<g
id="g3813">
<path
id="path3811"
d="M 14.2,1.8 V 14.2 H 1.8 V 1.8 h 12.4 m 1,-1 H 0.8 v 14.4 h 14.4 z"
class="st0"
style="fill:#414042" />
</g>
<line
id="line3815"
y2="4.3000002"
x2="12.8"
y1="4.3000002"
x1="3.2"
class="st1"
style="fill:none;stroke:#404040;stroke-width:2;stroke-miterlimit:10" />
<line
id="line3817"
y2="7.8000002"
x2="12.8"
y1="7.8000002"
x1="3.2"
class="st1"
style="fill:none;stroke:#404040;stroke-width:2;stroke-miterlimit:10" />
<line
id="line3819"
y2="11.6"
x2="12.8"
y1="11.6"
x1="3.2"
class="st1"
style="fill:none;stroke:#404040;stroke-width:2;stroke-miterlimit:10" />
<rect
id="rect3821"
height="16"
width="15.8"
class="st2"
x="0"
y="0"
style="fill:none" />
</g>
<g
id="ID0.7310718330554664"
transform="matrix(1.6136643,0,0,1.6136643,62.827571,8.4928323)">
<g
id="ID0.38590917782858014">
<path
style="fill:#9c9c9c;stroke:none"
id="ID0.3246160401031375"
d="m 28,195 h 30 v 8 H 28 Z"
transform="matrix(0.3575395,-0.46286663,0.6208511,0.47957394,-122.2,-58.65)" />
<path
style="fill:#9c9c9c;stroke:none;stroke-linecap:round"
id="ID0.7747227218933403"
d="M 265.25,79.2 270,80 266.75,87.8 Z"
transform="matrix(1.0236242,0.78098851,-0.42776972,0.56066829,-229.35,-228.7)" />
<path
style="fill:#9c9c9c;stroke:none"
id="path3861"
d="m 28,195 h 30 v 8 H 28 Z"
transform="matrix(0.10146888,-0.13136046,0.6622411,0.51154554,-111.7,-89.35)" />
</g>
</g>
</g>
</svg>

+ 24
- 0
website_form_builder/static/src/css/website_form_builder.less View File

@@ -0,0 +1,24 @@
/* Copyright 2017 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */

.o_website_form_builder {
.o_required {
> label::after {
// TODO Fix
content: '*';
color: @brand-danger;
}
}

.form-field.css_non_editable_mode_hidden {
opacity: 0.6;
}
}

body .modal.o_website_modal {
textarea, select[multiple] {
&.form-control {
height: auto;
}
}
}

+ 507
- 0
website_form_builder/static/src/js/snippets.js View File

@@ -0,0 +1,507 @@
/* Copyright 2017 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */

odoo.define('website_form_builder.snippets', function (require) {
"use strict";

var ajax = require("web.ajax");
var base = require('web_editor.base');
var core = require('web.core');
var data = require("web.data");
var Model = require('web.Model');
var options = require('web_editor.snippets.options');
var widgets = require("website_form_builder.widgets");
var _t = core._t;

var _fields_asked = {},
_fields_def = {},
_models_asked = false,
_models_def = $.Deferred(),
_templates_loaded = ajax.loadXML(
"/website_form_builder/static/src/xml/snippets.xml",
core.qweb
);

/**
* Lazily ask just once for models.
*
* @returns {$.Deferred} Indicates models were loaded.
*/
function available_models() {
if (!_models_asked) {
new Model("ir.model").call("search_read", {
domain: [
["website_form_access", "=", true],
],
fields: [
"name",
"model",
"website_form_label",
],
order: "website_form_label",
context: base.get_context(),
}).done(function (models_list) {
_models_def.resolve(_.indexBy(models_list, "model"));
});
_models_asked = true;
}
return _models_def;
}

/**
* Lazily load just once authorized fields for given model.
*
* @param {String} model Model technical name.
* @returns {$.Deferred} Indicates fields were loaded.
*/
function authorized_fields(model) {
if (!_fields_asked[model]) {
_fields_def[model] = $.Deferred();
available_models().done(function (models) {
new Model("ir.model").call(
"get_authorized_fields", [models[model].id], {
context: base.get_context()
}
).done($.proxy(
_fields_def[model].resolve,
_fields_def[model]
));
});
_fields_asked[model] = true;
}
return _fields_def[model];
}

var Field = options.Class.extend({
/**
* Disables the action buttons forbidden for current field.
*
* It loads overlay selectors to disable from the `data-disable`
* attribute set when using the option.
*/
start: function () {
this._super.apply(this, arguments);
this.$inputs = this.$(".o_website_form_input");
if (this.data.disable) {
this.disable_buttons(this.data.disable);
}
// Cross-browser editable labels
// HACK https://bugzilla.mozilla.org/show_bug.cgi?id=853519
this.$("label").prop("contentEditable", false)
.children("span").prop("contentEditable", true);
},

toggle_class: function (type, value) {
this._super.apply(this, arguments);
// Toggle field required attribute to match the container class
if (type === "reset" || value === "o_required") {
this.$inputs.attr(
"required",
this.$target.hasClass("o_required")
);
}
// Ask for a default value if hiding a field without it
if (
type === "click" &&
value === "css_non_editable_mode_hidden" &&
this.$target.hasClass(value) &&
// Query to know if there's a default value
!this.$inputs.filter(
// A selectable input is selected...
":checkbox[selected], :radio[selected]," +
"select>option[selected]," +
// ... or a fillable input is filled
"input[value][value!=''],textarea:parent"
).length
) {
this.ask_default_value(type);
}
},

/**
* Prompt the user for a default value for this field.
*
* @param {String} type Event type
* @returns {Dialog} Opened dialog
*/
ask_default_value: function (type) {
if (type === "reset") {
// Nothing to reset here
return;
}
var form = new widgets.DefaultValueForm(this, {}, this.$target);
form.on("save", this, this.set_default_value);
return form.open();
},

/**
* Set the new default value for the field.
*
* @param {Array|String} default_value It will be a `String` indicating
* the new default value, unless `this.$input` is a checkbox, in which
* case it will be an `Array` that contains the value of the check
* boxes that must be enabled by default.
*/
set_default_value: function (default_value) {
var $inputs = this.$inputs;
if ($inputs.is(":checkbox,:radio")) {
// Set as checked chosen boxes
$inputs.each(function () {
$(this).attr(
"checked",
$.inArray($(this).val(), default_value) !== -1
);

});
} else if ($inputs.is("select")) {
// Set as selected chosen option
$inputs.find("option").each(function () {
$(this).attr(
"selected",
$(this).attr("value") === default_value
);
});
} else {
// Simply put the new default value in the element
$inputs.attr("value", default_value || "");
}
},

/**
* Disables an action button.
*
* @param {String} selector It should be `.oe_snippet_move`,
* `.oe_snippet_clone`, or `.oe_snippet_remove`
* (or comma-separated combinations of them).
*/
disable_buttons: function (selector) {
var button = this.$overlay.find(selector);
button.addClass("disabled");
},
});

var Form = options.Class.extend({
init: function () {
this._super.apply(this, arguments);
this.$form = this.$("form.s_website_form");
},

clean_for_save: function () {
var fields = this.present_fields();
// Sync HTML metadata of custom fields with UI
this.$("[data-model-field=false]").each(function () {
var $el = $(this),
$label = $el.children(".control-label"),
$input = $el.find(".o_website_form_input");
if (!$label.length) {
return;
}
$input.attr("name", _.str.clean($label.text()));
$input.filter(":checkbox, :radio").each(function () {
var $box = $(this);
$box.attr(
"value",
_.str.clean($box.closest("label").text())
);
});
});
// Remove any content in the form result
this.$("#o_website_form_result").removeAttr("class").empty();
// Do not save disabled send button
this.$(".o_website_form_send").removeClass("disabled");
// Do not save fields error status
this.$(".has-error").removeClass("has-error");
if (fields.length) {
// Whitelist model fields found in current form
new Model("ir.model.fields").call(
"formbuilder_whitelist", [this.controller_data().model_name, fields], {
context: base.get_context()
}, {
// Do not save until done
async: false
}
);
} else {
// No fields? Destroy snippet before saving
this.$target.remove();
}
},

/**
* Ask for a model or remove snippet.
*/
drop_and_build_snippet: function () {
this.ask_model();
this._super.apply(this, arguments);
},

/**
* Fetch available models and let user choose one.
*
* @param {String} type Event type
* @returns {$.Deferred} Resolves with the open form
*/
ask_model: function (type) {
if (type === "reset") {
// Nothing to reset here
return;
}
return available_models().done($.proxy(this._ask_model, this));
},

/**
* Create and process form widget for asking the model.
*
* @param {Object} models ORM records of ir.model objects
* @returns {Dialog} Open dialog
*/
_ask_model: function (models) {
var form = new widgets.ParamsForm(
this, {}, models, this.controller_data().model_name
);
form.on("save cancel", this, this.set_model);
return form.open();
},

/**
* Fetch available fields from model and let user choose one.
*
* @param {String} type Event type
* @returns {$.Deferred} Resolves with the open dialog
*/
ask_model_field: function (type) {
if (type === "reset") {
// Nothing to reset here
return;
}
return authorized_fields(this.controller_data().model_name).done(
$.proxy(this._ask_model_field, this)
);
},

/**
* Create and process form widget for choosing the new model field.
*
* @param {Array} fields Fields among which user can choose
* @returns {Dialog} Open dialog
*/
_ask_model_field: function (fields) {
var form = new widgets.ModelFieldForm(
this, {}, fields, this.present_fields()
);
form.on("save", this, function (infos) {
_.map(infos, this.add_model_field, this);
});
return form.open();
},

/**
* Inject a new custom field into the form.
*
* @param {String} type Event type
* @param {String} value Custom field type to add
* @param {jQuery} $li Clicked menu item
* @returns {jQuery} Added field
*/
add_custom_field: function (type, value, $li) {
if (type === "reset") {
// Nothing to reset here
return;
}
var name = _.str.sprintf(
_t("Custom %s field"),
_.str.clean($li.text())
),
option = _t("Option %d"),
field = {
required: false,
help: _.str.sprintf(
_t("%s help block"),
name
),
string: name,
// Default values for selection fields
selection: _.map(_.range(1, 5), function (num) {
return [null, _.str.sprintf(option, num)];
}),
type: value,
};
return this._add_field(
_.str.sprintf("website_form_builder.field.%s", value),
name,
field,
// Default values for many2* fields
_.map(_.range(1, 5), function (num) {
return {
id: null,
display_name: _.str.sprintf(option, num),
};
}),
false
);
},

/**
* Get current form's controller data.
*
* @returns {Object} Form-attached data that is used by the
* `website_form.animation` JS module. Check its source code to know
* what they do.
*/
controller_data: function () {
var hidden_data = {},
attributes = Array.prototype.slice.call(
this.$form[0].attributes);
for (var attr in attributes) {
attr = attributes[attr];
if (_.str.startsWith(attr.name, 'data-form_field_')) {
hidden_data[attr.name.substr(16)] = attr.value;
}
}
return {
force_action: this.$form.attr("data-force_action"),
hidden_data: hidden_data,
model_name: this.$form.attr("data-model_name"),
success_page: this.$form.attr("data-success_page"),
};
},

/**
* @returns {Array} List of present field names
*/
present_fields: function () {
return _.pluck(this.$(":input[name]"), "name");
},

/**
* Change form's target model.
*
* @param {String} model Technical model name
*/
set_model: function (model) {
var previous_model = this.controller_data().model_name;
if (!model && !previous_model) {
// No model? Destroy snippet
this.$target.remove();
return;
}
this.$form.attr("data-model_name", model);
// Model changed? Load new fields and reset snippet
if (previous_model !== model) {
authorized_fields(model)
.done($.proxy(this.reset_model_fields, this));
}
},

/**
* Empty form's current fields and fill with only required ones.
*
* @param {Object} fields ORM ir.model.fields records
*/
reset_model_fields: function (fields) {
// Remove old model fields
this.$(".o_website_form_fields [data-model-field=true]").remove();
// Add new model required fields by default
for (var name in fields) {
if (fields[name].required) {
this.add_model_field({
name: name,
field: fields[name]
});
}
}
},

/**
* Inject a new field from the model into the form.
*
* @param {Object} info {name: field_name, field: field_definition}
* @returns {$.Deferred} Resolves with the added field jQuery element
*/
add_model_field: function (info) {
var relational_data = [],
template = _.str.sprintf(
"website_form_builder.field.%s",
info.field.type
);
if (info.field.type.indexOf("many") !== -1) {
relational_data = this.relational_options(info.field);
}
return $.when(template, info.name, info.field, relational_data,
true, _templates_loaded)
.done($.proxy(this._add_field, this));
},

/**
* Perform insertion of field in form.
*
* @param {String} template QWeb field template name to be rendered
* @param {String} name Field name
* @param {Object} field Field attributes
* @param {Array} relational_data Data for x2x fields
* @param {Boolean} model_field Is it a model field?
* @returns {jQuery} Appended element
*/
_add_field: function (template, name, field, relational_data,
model_field) {
return this.$(".o_website_form_fields").append(core.qweb.render(
template, {
field: field,
model_field: model_field,
name: name,
relational_data: relational_data,
required_att: field.required
? "required"
: null,
widget: this,
}
));
},

/**
* Selectable options for relational fields.
*
* @param {String} field Field name
* @returns {$.Deferred} ORM results
*/
relational_options: function (field) {
var domain = [],
context = base.get_context();
// Domain might contain un-evaluable literals
try {
domain = new data.CompoundDomain(field.domain || []).eval();
} catch (error) {
// eslint-disable-next-line no-console
console.warn("Cannot evaluate field domain, ignoring.");
}
// Context too
try {
context = new data.CompoundContext(
base.get_context(),
field.context
).eval();
} catch (error) {
// eslint-disable-next-line no-console
console.warn("Cannot evaluate field context, using user's.");
}
// Get results
return new Model(field.relation).call("search_read", {
domain: domain,
fields: ["display_name"],
order: "display_name",
context: context,
});
},
});

// Add options to registry
options.registry.website_form_builder_field = Field;
options.registry.website_form_builder_form = Form;

return {
authorized_fields: authorized_fields,
available_models: available_models,
Field: Field,
Form: Form,
};
});

+ 159
- 0
website_form_builder/static/src/js/tour.js View File

@@ -0,0 +1,159 @@
/* Copyright 2017 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */

odoo.define("website_form_builder.tour", function (require) {
"use strict";

// Dependencies here by alphabetic order. Template only for Odoo 9+.
var tour = require("web_tour.tour");
var base = require("web_editor.base");

function show_submenus() {
$(".oe_overlay_options:visible .dropdown-menu").addClass("show");
}

function hide_submenus() {
$(".oe_overlay_options .dropdown-menu").removeClass("show");
}

var options = {
url: "/",
skip_enabled: true,
test: true,
wait_for: base.ready(),
},
steps = [
{
trigger: "#oe_main_menu_navbar a[data-action=edit]",
},
{
run: "drag_and_drop",
trigger: ".oe_snippet:has(.o_website_form_builder) .oe_snippet_thumbnail",
},
{
run: "text res.country",
trigger: ".modal-dialog #model",
},
{
trigger: ".modal-dialog .o_save_button",
},
{
trigger: ".s_website_form[data-model_name='res.country']",
},
{
trigger: ".oe_overlay_options:visible .btn:contains('Customize')",
},
{
trigger: ".oe_overlay_options:visible [data-ask_model_field]>a",
},
{
run: "text name",
trigger: ".modal-dialog #field",
},
{
trigger: ".modal-dialog .o_save_button",
},
{
trigger: "input[name=name]",
},
{
trigger: ".oe_overlay_options:visible .oe_snippet_remove",
},
{
trigger: ".s_website_form[data-model_name='res.country']",
},
{
trigger: ".oe_overlay_options:visible .btn:contains('Customize')",
},
{
trigger: ".oe_overlay_options:visible [data-ask_model_field]>a",
},
{
run: "text name",
trigger: ".modal-dialog #field",
},
{
trigger: ".modal-dialog .o_save_button",
},
{
trigger: ".form-field label[for=name]",
},
{
trigger: ".oe_overlay_options:visible .btn:contains('Customize')",
},
{
trigger: ".oe_overlay_options:visible [data-ask_default_value]>a",
},
{
run: "text Monkey Island",
trigger: ".modal-dialog [name=name]",
},
{
trigger: ".modal-dialog .o_save_button",
},
{
trigger: ".s_website_form[data-model_name='res.country']",
},
{
trigger: ".oe_overlay_options:visible .btn:contains('Customize')",
},
{
run: show_submenus,
trigger: ".oe_overlay_options:visible .snippet-option-website_form_builder_form:has(.dropdown-menu)",
},
{
trigger: '.oe_overlay_options:visible [data-add_custom_field="selection-radio"]>a',
},
{
run: hide_submenus,
trigger: ".form-field-selection-radio",
},
{
trigger: ".oe_overlay_options:visible .btn:contains('Customize')",
},
{
trigger: ".oe_overlay_options:visible [data-ask_model]>a",
},
{
run: "text res.currency",
trigger: ".modal-dialog #model",
},
{
trigger: ".modal-dialog .o_save_button",
},
{
trigger: ".form-field-selection-radio",
},
{
trigger: ".oe_overlay_options:visible .oe_snippet_clone",
},
{
trigger: ".oe_overlay_options:visible .oe_snippet_remove",
},
{
trigger: "#web_editor-top-edit [data-action=save]",
},
{
run: "text Monkey Island Dollars",
trigger: "body:not(.editor_enable) .s_website_form[data-model_name='res.currency'] input[name=name]",
},
{
trigger: ".o_website_form_send",
},
{
trigger: "#o_website_form_result.text-danger",
},
{
run: "text 🐵",
trigger: "body:not(.editor_enable) .s_website_form[data-model_name='res.currency'] .has-error input[name=symbol]",
},
{
trigger: ".o_website_form_send",
},
{
trigger: "#o_website_form_result.text-success",
},
];

tour.register("website_form_builder.tour", options, steps);
});

+ 162
- 0
website_form_builder/static/src/js/widgets.js View File

@@ -0,0 +1,162 @@
/* Copyright 2017 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */

odoo.define('website_form_builder.widgets', function (require) {
"use strict";

var ajax = require("web.ajax");
var core = require('web.core');
var widget = require("web_editor.widget");
var _t = core._t;
var Dialog = widget.Dialog;

var result = $.Deferred(),
_templates_loaded = ajax.loadXML(
"/website_form_builder/static/src/xml/widgets.xml",
core.qweb
);

var DefaultValueForm = Dialog.extend({
template: "website_form_builder.DefaultValueForm",

/**
* Store needed field information.
*
* @param {Object} parent Widget where this dialog is attached
* @param {Object} options Dialog creation options
* @param {DOMElement} field Field asking for a new default value
* @returns {Dialog} New Dialog object
*/
init: function (parent, options, field) {
this.field_html = $(field).html();
var _options = $.extend({}, {
title: _t("Set field's default value"),
size: "small",
}, options);
return this._super(parent, _options);
},

/**
* Save the new default value.
*/
save: function () {
var inputs = this.$(".o_website_form_input");
if (inputs.is(":checkbox")) {
this.final_data = inputs.filter(":checked")
.map(function () {
return $(this).val();
})
.get();
} else {
this.final_data = inputs.val();
}
this._super.apply(this, arguments);
},
});

var ParamsForm = Dialog.extend({
template: "website_form_builder.ParamsForm",

/**
* Store models info before creating widget
*
* @param {Object} parent Widget where this dialog is attached
* @param {Object} options Dialog creation options
* @param {Array} models Available models to choose among
* @param {String} chosen Prechosen model
* @returns {Dialog} New Dialog object
*/
init: function (parent, options, models, chosen) {
this.models = models;
this.chosen = chosen;
var _options = $.extend({}, {
title: _t("Form Settings"),
size: "small",
}, options);
return this._super(parent, _options);
},

/**
* Save new model
*/
save: function () {
this.final_data = this.$("#model").val();
this._super.apply(this, arguments);
},
});

var ModelFieldForm = Dialog.extend({
template: "website_form_builder.ModelFieldForm",

/**
* Store fields info before creating widget
*
* @param {Object} parent Widget where this dialog is attached
* @param {Object} options Dialog creation options
* @param {Array} fields Model's fields
* @param {Array} blacklist Fields that cannot be chosen
* @returns {Dialog} New Dialog object
*/
init: function (parent, options, fields, blacklist) {
this.fields = fields;
this.blacklist = blacklist;
var _options = $.extend({}, {
title: _t("Add Model Fields"),
size: "small",
}, options);
return this._super(parent, _options);
},

/**
* Save field dict
*/
save: function () {
var names = this.$("#field").val();
this.final_data = [];
for (var n in names) {
var name = names[n];
this.final_data.push({
name: name,
field: this.fields[name],
});
}
this._super.apply(this, arguments);
},

/**
* Get filtered fields sorted by string.
*
* @returns {Array} [[string, name], ...]
*/
sorted_fields: function () {
var _result = [];
for (var name in this.fields) {
var field = this.fields[name];
if (
name === "id" ||
field.readonly ||
field.type === "one2many" ||
field.type === "reference" ||
field.type === "serialized" ||
this.blacklist.indexOf(name) !== -1
) {
continue;
}
_result.push([field.string, name]);
}
_result.sort();
return _result;
},
});

// Resolve when finished loading templates
_templates_loaded.done(function () {
result.resolve({
DefaultValueForm: DefaultValueForm,
ParamsForm: ParamsForm,
ModelFieldForm: ModelFieldForm,
});
});

return result;
});

+ 214
- 0
website_form_builder/static/src/xml/snippets.xml View File

@@ -0,0 +1,214 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->

<template>
<t t-name="website_form_builder.field_wrapper">
<div
t-attf-class="form-group form-field #{field.required ? 'o_required' : ''} form-field-#{field.type}"
t-att-data-model-field="model_field"
t-att-data-optional="!field.required"
>
<t t-raw="0"/>
<t t-if="field.help">
<p class="help-block">
<t t-esc="field.help"/>
</p>
</t>
</div>
</t>

<t t-name="website_form_builder.field_label">
<t t-call="website_form_builder.field_wrapper">
<label class="control-label" t-att-for="name">
<span>
<t t-esc="field.string"/>
</span>
</label>
<t t-raw="0"/>
</t>
</t>

<t t-name="website_form_builder.field.binary">
<t t-call="website_form_builder.field_label">
<input
t-att-name="name"
t-att-required="required_att"
type="file"
class="o_website_form_input"
/>
</t>
</t>

<t t-name="website_form_builder.field.boolean">
<t t-call="website_form_builder.field_wrapper">
<label class="control-label" t-att-for="name">
<input
t-att-name="name"
t-att-required="required_att"
type="checkbox"
class="o_website_form_input"
value="1"
/>
<t t-esc="field.string"/>
</label>
</t>
</t>

<t t-name="website_form_builder.field.char">
<t t-call="website_form_builder.field_label">
<input
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input"
t-att-maxlength="field.size || null"
type="text"
/>
</t>
</t>

<t t-name="website_form_builder.field.date">
<t t-call="website_form_builder.field_label">
<input
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input o_website_form_date"
type="text"
/>
</t>
</t>

<t t-name="website_form_builder.field.datetime">
<t t-call="website_form_builder.field_label">
<input
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input o_website_form_datetime"
type="text"
/>
</t>
</t>

<t t-name="website_form_builder.field.float">
<t t-call="website_form_builder.field_label">
<input
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input"
type="number"
step="0.01"
/>
</t>
</t>

<t t-name="website_form_builder.field.html">
<!-- TODO Use a safe HTML widget instead -->
<t t-call="website_form_builder.text"/>
</t>

<t t-name="website_form_builder.field.integer">
<t t-call="website_form_builder.field_label">
<input
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input"
type="number"
/>
</t>
</t>

<t t-name="website_form_builder.field.many2many">
<t t-call="website_form_builder.field_label">
<t t-foreach="relational_data" t-as="option">
<div class="checkbox">
<label class="control-label">
<input
t-att-name="name"
t-att-required="required_att"
t-att-value="option.id"
type="checkbox"
class="o_website_form_input"
/>
<span>
<t t-esc="option.display_name"/>
</span>
</label>
</div>
</t>
</t>
</t>

<t t-name="website_form_builder.field.many2one">
<t t-call="website_form_builder.field_label">
<select
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input"
>
<option/>
<t t-foreach="relational_data" t-as="option">
<option t-att-value="option.id">
<t t-esc="option.display_name"/>
</option>
</t>
</select>
</t>
</t>

<t t-name="website_form_builder.field.monetary">
<!-- TODO Find a way to sanely guess the default currency and position
to use a beautiful Bootstrap input group instead -->
<t t-call="website_form_builder.float"/>
</t>

<!-- Only used in model fields -->
<t t-name="website_form_builder.field.selection">
<t t-call="website_form_builder.field_label">
<select
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input"
>
<t t-foreach="field.selection" t-as="option">
<option t-att-value="option[0]">
<t t-esc="option[1]"/>
</option>
</t>
</select>
</t>
</t>

<!-- Only used in custom fields -->
<t t-name="website_form_builder.field.selection-radio">
<t t-call="website_form_builder.field_label">
<t t-foreach="relational_data" t-as="option">
<div class="radio">
<label class="control-label">
<input
t-att-name="name"
t-att-required="required_att"
t-att-value="option.id"
type="radio"
class="o_website_form_input"
/>
<span>
<t t-esc="option.display_name"/>
</span>
</label>
</div>
</t>
</t>
</t>

<t t-name="website_form_builder.field.text">
<t t-call="website_form_builder.field_label">
<textarea
t-att-name="name"
t-att-required="required_att"
class="form-control o_website_form_input"
t-att-maxlength="field.size || null"
type="text"
/>
</t>
</t>
</template>

+ 44
- 0
website_form_builder/static/src/xml/widgets.xml View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->

<template>
<t t-name="website_form_builder.DefaultValueForm">
<form>
<t t-raw="widget.field_html"/>
</form>
</t>

<t t-name="website_form_builder.ModelFieldForm">
<form>
<div class="form-group">
<label for="field">Choose the new field for the form</label>
<select id="field" class="form-control" multiple="">
<t t-foreach="widget.sorted_fields()" t-as="field_pair">
<option t-att-value="field_pair[1]">
<t t-esc="field_pair[0]"/>
</option>
</t>
</select>
</div>
</form>
</t>

<t t-name="website_form_builder.ParamsForm">
<form>
<div class="form-group">
<label for="model">Choose this form's action</label>
<select id="model" class="form-control">
<t t-foreach="widget.models" t-as="model">
<option
t-att-value="model"
t-att-selected="model === widget.chosen ? 'selected' : null"
>
<t t-esc="widget.models[model].website_form_label || widget.models[model].name"/>
</option>
</t>
</select>
</div>
</form>
</t>
</template>

+ 26
- 0
website_form_builder/templates/assets.xml View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->

<odoo>

<template id="assets_editor" inherit_id="website.assets_editor">
<xpath expr=".">
<script type="text/javascript"
src="/website_form_builder/static/src/js/snippets.js"/>
<script type="text/javascript"
src="/website_form_builder/static/src/js/widgets.js"/>
<link rel="stylesheet"
href="/website_form_builder/static/src/css/website_form_builder.less"/>

<!-- TODO: Fix upstream -->
<script type="text/javascript"
src="/web/static/src/js/framework/data.js"/>
<script type="text/javascript"
src="/web/static/lib/py.js/lib/py.js"/>
<script type="text/javascript"
src="/web/static/src/js/framework/pyeval.js"/>
</xpath>
</template>

</odoo>

+ 156
- 0
website_form_builder/templates/snippets.xml View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->

<odoo>

<!-- Snippets body -->
<template id="s_website_form" name="Form">
<section class="container readable o_website_form_builder">
<form
accept-charset="UTF-8"
action="/website_form/"
class="s_website_form col-md-12 mt32"
data-model_name=""
data-force_action=""
data-success_page=""
enctype="multipart/form-data"
method="post"
>
<div class="o_website_form_fields">
<div class="o_not_editable hidden" t-translation="off">
This placeholder prevents its parent from
disappearing when emptied
</div>
</div>
<div class="form-group">
<button type="button"
class="btn btn-primary btn-lg o_website_form_send o_default_snippet_text">
Send
</button>
<span id="o_website_form_result"/>
</div>
</form>
</section>
</template>

<!-- Add snippets to menu -->
<template id="snippets" inherit_id="website.snippets">
<xpath expr="//*[@id='snippet_structure']//*[hasclass('o_panel_body')]">
<t t-snippet="website_form_builder.s_website_form"
t-thumbnail="/website_form_builder/static/description/icon.png"/>
</xpath>
</template>

<!-- Add snippets options -->
<template id="snippet_options" inherit_id="website.snippet_options">
<xpath expr=".">
<!-- The form itself -->
<div
data-js="website_form_builder_form"
data-selector=".o_website_form_builder"
>
<li data-only="click" data-ask_model="">
<a tabindex="-1"><i class="fa fa-cogs"/> Change action</a>
</li>
<li data-only="click" data-ask_model_field="">
<a tabindex="-1"><i class="fa fa-plus"/> Add model fields</a>
</li>
<li class="dropdown-submenu">
<a tabindex="-1"><i class="fa fa-hand-spock-o"/> Add custom field</a>
<ul class="dropdown-menu">
<li data-only="click" data-add_custom_field="char">
<a tabindex="-1">Short text</a>
</li>
<li data-only="click" data-add_custom_field="text">
<a tabindex="-1">Long text</a>
</li>
<li data-only="click" data-add_custom_field="selection-radio">
<a tabindex="-1">Single selection</a>
</li>
<li data-only="click" data-add_custom_field="many2many">
<a tabindex="-1">Multiple selection</a>
</li>
<li data-only="click" data-add_custom_field="boolean">
<a tabindex="-1">Yes or not</a>
</li>
<li data-only="click" data-add_custom_field="integer">
<a tabindex="-1">Whole number</a>
</li>
<li data-only="click" data-add_custom_field="float">
<a tabindex="-1">Decimal number</a>
</li>
<li data-only="click" data-add_custom_field="date">
<a tabindex="-1">Date</a>
</li>
<li data-only="click" data-add_custom_field="datetime">
<a tabindex="-1">Date and time</a>
</li>
<li data-only="click" data-add_custom_field="binary">
<a tabindex="-1">File upload</a>
</li>
</ul>
</li>
</div>

<!-- Enable moving fields around and deleting them by default -->
<div
data-selector=".o_website_form_builder .form-field"
data-drop-in=".o_website_form_builder .o_website_form_fields"
/>
<div
data-selector=".o_website_form_builder .form-field .checkbox"
data-drop-near=".o_website_form_builder .form-field .checkbox"
/>
<div
data-selector=".o_website_form_builder .form-field .radio"
data-drop-near=".o_website_form_builder .form-field .radio"
/>

<!-- Allow to set default values -->
<div
data-js="website_form_builder_field"
data-selector=".o_website_form_builder .form-field:not(.form-field-binary)"
>
<li data-only="click" data-ask_default_value="">
<a tabindex="-1">Set default value</a>
</li>
</div>

<!-- Allow user to set additional required fields -->
<div
data-js="website_form_builder_field"
data-selector=".o_website_form_builder .form-field[data-optional=true]"
>
<li data-toggle_class="o_required">
<a tabindex="-1">Set as required</a>
</li>
</div>

<!-- Allow to hide fields -->
<div
data-js="website_form_builder_field"
data-selector=".o_website_form_builder .form-field"
>
<li data-toggle_class="css_non_editable_mode_hidden">
<a tabindex="-1"><i class="fa fa-eye-slash"/> Hide field</a>
</li>
</div>

<!-- Cannot remove required model fields -->
<div
data-js="website_form_builder_field"
data-selector=".o_website_form_builder .form-field[data-model-field=true][data-optional=false]"
data-disable=".oe_snippet_remove"
/>

<!-- Cannot duplicate model fields -->
<div
data-js="website_form_builder_field"
data-selector=".o_website_form_builder .form-field[data-model-field=true]"
data-disable=".oe_snippet_clone"
/>
</xpath>
</template>

</odoo>

+ 4
- 0
website_form_builder/tests/__init__.py View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from . import test_ui

+ 20
- 0
website_form_builder/tests/test_ui.py View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Tecnativa - Jairo Llopis
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from odoo.tests.common import HttpCase


class UICase(HttpCase):
def test_ui_website(self):
"""Test frontend tour."""
tour = (
"odoo.__DEBUG__.services['web_tour.tour']",
"website_form_builder.tour",
)
self.phantom_js(
url_path="/",
code="%s.run('%s')" % tour,
ready="%s.tours['%s'].ready" % tour,
login="admin",
timeout=60)

+ 40
- 0
website_form_builder/views/ir_model.xml View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->

<odoo>

<record id="view_model_form" model="ir.ui.view">
<field name="name">website_form features</field>
<field name="model">ir.model</field>
<field name="inherit_id" ref="base.view_model_form"/>
<field name="arch" type="xml">
<!-- Model website_form management -->
<xpath expr="//field[@name='model']/../..">
<group name="website_form" string="Website Forms">
<field name="website_form_access"/>
<field name="website_form_label"/>
<field name="website_form_default_field_id"/>
</group>
</xpath>

<!-- Model fields subform -->
<xpath expr="//field[@name='field_id']//field[@name='readonly']"
position="after">
<field name="website_form_blacklisted"/>
</xpath>
</field>
</record>

<record id="view_model_fields_form" model="ir.ui.view">
<field name="name">website_form features</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='readonly']" position="after">
<field name="website_form_blacklisted"/>
</xpath>
</field>
</record>

</odoo>

Loading…
Cancel
Save